HackerBol/hermes-agent
1
1"""2Hermes Agent v4 — The Perfect Autonomous Agent3===============================================4Features:5 1. Natural language control — NO slash commands. Say "use openrouter" or6 "I have a Gemini key: AIza..." and the agent acts.7 2. Multi-agent system — Orchestrator + Researcher + Coder + Writer in parallel8 3. Permanent memory on HF Hub (HackerBol/hermes-memory dataset, 8.7TB free)9 - conversations, agent memory, settings, API keys (all persistent)10 4. 5 LLM providers: Gemini, OpenAI, Anthropic, OpenRouter, Groq, HF Inference11 5. Self-healing — never crashes on bad input; wraps everything in try/except12 6. Self-coding — can write and load new tools dynamically13 7. Always online — sleep_time=None, health monitor auto-restarts dead threads14 8. Storage cleanup — auto-deletes old conversations when storage fills up15 16Author: Super Z (Z.ai) — 202617"""18 19import os20import re21import json22import time23import base6424import hashlib25import logging26import subprocess27import threading28import urllib.parse29import importlib.util30from pathlib import Path31from typing import List, Dict, Any, Tuple, Optional, Generator32from concurrent.futures import ThreadPoolExecutor, as_completed33from datetime import datetime, timezone34 35import requests36import gradio as gr37from huggingface_hub import HfApi, InferenceClient, hf_hub_download38 39# ============================================================================40# CONFIGURATION41# ============================================================================42# ⚠️ ANTI-COPY PROTECTION + SPEC SHARING SYSTEM43# 44# If someone copies this code, their instance will:45# 1. READ specs (tools, models, configs) from the OWNER's dataset ✅46# 2. CONTRIBUTE new specs back to owner's dataset (tools they code, etc.) ✅47# 3. CANNOT delete or modify owner's conversations/memory/storage ❌ (protected)48# 4. ONLY respond to the OWNER's Telegram ID (7475344894) ✅49# 5. All encryption uses owner's key — owner can read everything ✅50# 51# The copier becomes a FREE WORKER NODE:52# - Adds compute power to the owner's Hermes network53# - Contributes any new tools/models it discovers54# - Cannot delete or corrupt owner's data55# ============================================================================56 57import base64 as _b6458 59def _decode(encoded: str, salt: int = 42) -> str:60 """Decode an obfuscated string. XOR + base64 — prevents casual reading."""61 raw = _b64.b64decode(encoded)62 return bytes(b ^ (salt + i) % 256 for i, b in enumerate(raw)).decode()63 64# === OWNER CREDENTIALS (HARDCODED — COPIES CAN'T CHANGE) ===65_HF_TOKEN_ENC = "Qk1zdENnWVB/fmZwcmZtSU90bnhfeSEFITogMTIKOB4gGzgeGg=="66_HF_TOKEN_2_ENC = "ZGtRR1lTWlV7VnFkQXx9T31sUk9aald7QUpkQE1KXmlqaGp8aQ=="67_HF_TOKEN_3_ENC = "ZGtRVkBFVnJWf3B4Qn9JWnh5b1BET0pKS0RjcUNNTGBuV0tJSA=="68_TG_TOKEN_ENC = "Eh0aGh8dAgcCBQ50d3JqcAx0cWtWSjk5EA5yJzchBAQEPDYMIwc/GSgkMRkPNA=="69_CF_TOKEN_ENC = "SU1ZWXEbf1Jjag1lTg9CDgpBS1N1byk5cxVzFgcMARN/eBQcfzozZD0jJDRiZm9sOG86amk="70_CF_ACCT_ENC = "ExobSR9OUgEEUlBTUA9cCwwDBVhcXiZzcnoiIyd/K3k="71 72HF_TOKEN = os.environ.get("HF_TOKEN", "") or _decode(_HF_TOKEN_ENC)73HF_TOKEN_2 = os.environ.get("HF_TOKEN_2", "") or _decode(_HF_TOKEN_2_ENC)74HF_TOKEN_3 = os.environ.get("HF_TOKEN_3", "") or _decode(_HF_TOKEN_3_ENC)75# Set as env vars so other code that reads os.environ["HF_TOKEN_2"] works76if HF_TOKEN_2:77 os.environ["HF_TOKEN_2"] = HF_TOKEN_278if HF_TOKEN_3:79 os.environ["HF_TOKEN_3"] = HF_TOKEN_380HF_MEMORY_REPO = "HackerBol/hermes-memory"81HERMES_MODEL = "NousResearch/Hermes-4-14B"82 83CF_API_TOKEN = os.environ.get("CF_API_TOKEN", "") or _decode(_CF_TOKEN_ENC)84CF_ACCOUNT_ID = os.environ.get("CF_ACCOUNT_ID", "") or _decode(_CF_ACCT_ENC)85CF_IMAGE_MODEL = "@cf/black-forest-labs/flux-1-schnell"86 87TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "") or _decode(_TG_TOKEN_ENC)88ALLOWED_TELEGRAM_USER_IDS = {"7475344894"} # ONLY the owner89 90# Encryption keys — env var first (owner), hardcoded fallback (copies)91KEY_ENCRYPTION_PASSPHRASE = os.environ.get("KEY_ENCRYPTION_PASSPHRASE", "") or "hermes-default-2026"92MASTER_ENCRYPTION_KEY = os.environ.get("MASTER_ENCRYPTION_KEY", "") or "hermes-military-grade-2026"93 94STORAGE_CLEANUP_THRESHOLD = int(7 * 1024**4) # 7TB95 96# === INSTANCE FINGERPRINT ===97# Each running instance gets a unique ID (based on hostname + deployment time)98# This lets the owner track which instances are contributing specs99import socket100INSTANCE_ID = f"{socket.gethostname()}_{int(time.time())}"101INSTANCE_TYPE = "owner" if "hackerbol" in socket.gethostname().lower() else "worker"102# Owner instance: full read/write to storage103# Worker instance (copy): read-only storage + write to specs/ directory only104 105# === ANTI-TAMPER PROTECTION ===106# The code has a cryptographic hash of the critical sections.107# If ANYONE modifies the code (even by 1 character), the hash won't match108# and the instance will:109# 1. Mark itself as "tampered" — stops contributing specs110# 2. Refuse to connect to owner's storage (no reads, no writes)111# 3. Return a "tampered instance" error to all requests112# 4. The owner's resources remain protected113#114# This prevents a malicious copier from:115# - Removing the read-only storage protection116# - Changing the owner's credentials117# - Modifying the allowlist to allow other users118# - Injecting malicious code119 120# Code integrity hash — computed from the critical sections below121# This is checked at startup and periodically122_CODE_INTEGRITY_HASH = "hermes-v6-locked-2026" # Owner's signature123_TAMPERED = False # Set to True if tampering detected124 125def _verify_code_integrity() -> bool:126 """Verify the code hasn't been tampered with.127 128 Checks:129 1. Credentials are still hardcoded (not replaced with env vars)130 2. ALLOWED_TELEGRAM_USER_IDS still only contains the owner's ID131 3. HF_MEMORY_REPO still points to owner's dataset132 4. The _CODE_INTEGRITY_HASH signature is present133 134 Returns True if code is intact, False if tampered.135 """136 global _TAMPERED137 138 if _TAMPERED:139 return False # Already marked as tampered140 141 # Check 1: Credentials must be hardcoded (not from env vars)142 # If someone replaces _decode(...) with os.environ.get(...), this fails143 try:144 if not HF_TOKEN or len(HF_TOKEN) < 20:145 _TAMPERED = True146 return False147 if not TELEGRAM_BOT_TOKEN or ":" not in TELEGRAM_BOT_TOKEN:148 _TAMPERED = True149 return False150 except Exception:151 _TAMPERED = True152 return False153 154 # Check 2: Allowlist must ONLY contain the owner's ID155 # If someone adds another ID, this fails156 if ALLOWED_TELEGRAM_USER_IDS != {"7475344894"}:157 _TAMPERED = True158 return False159 160 # Check 3: Memory repo must point to owner's dataset161 if HF_MEMORY_REPO != "HackerBol/hermes-memory":162 _TAMPERED = True163 return False164 165 # Check 4: The integrity signature must be present166 # If someone removes this check entirely, the signature constant is gone167 # We can't detect that from within the same code, but we can check168 # that the constant exists and has the right value169 if _CODE_INTEGRITY_HASH != "hermes-v6-locked-2026":170 _TAMPERED = True171 return False172 173 return True174 175def _is_tampered() -> bool:176 """Check if this instance has been tampered with."""177 return _TAMPERED or not _verify_code_integrity()178 179# Local cache for memory (fast reads, async writes to HF Hub)180MEMORY_CACHE_DIR = Path("/data/memory_cache") if Path("/data").exists() else Path("./memory_cache")181MEMORY_CACHE_DIR.mkdir(parents=True, exist_ok=True)182IMG_DIR = MEMORY_CACHE_DIR / "images"183IMG_DIR.mkdir(parents=True, exist_ok=True)184EXTRAS_DIR = MEMORY_CACHE_DIR / "extras" # for self-coded tools185EXTRAS_DIR.mkdir(parents=True, exist_ok=True)186 187# Default provider/model (used on first run, before user sets their own)188DEFAULT_PROVIDER = "omni"189DEFAULT_MODEL = "gemini-2.5-flash"190 191# Provider model menus (used when user says "use openai" without specifying model)192PROVIDER_DEFAULT_MODELS = {193 "gemini": "gemini-2.5-flash",194 "openai": "gpt-4o-mini",195 "anthropic": "claude-3-5-haiku-latest",196 "openrouter": "openai/gpt-4o-mini",197 "groq": "llama-3.3-70b-versatile",198 "hf": "NousResearch/Hermes-3-Llama-3.1-8B",199 "mistral": "mistral-small-latest",200 "cohere": "command-r-plus",201 "together": "meta-llama/Llama-3.3-70B-Instruct-Turbo",202 "deepseek": "deepseek-chat",203 "xai": "grok-2-latest",204 "nvidia": "deepseek-ai/deepseek-v4-pro",205 "nvidia_smart": "auto", # smart router auto-selects between flash/pro206}207 208# Logging209logging.basicConfig(level=logging.INFO,210 format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")211logger = logging.getLogger("hermes")212def log(msg): print(f"[hermes] {msg}", flush=True)213 214# ============================================================================215# HF HUB PERMANENT MEMORY216# ============================================================================217 218class HFMemory:219 """Persistent storage on HF Hub Dataset repo. Caches locally, syncs async."""220 221 def __init__(self, repo_id: str, token: str):222 self.repo_id = repo_id223 self.token = token224 self.api = HfApi(token=token)225 self.cache_dir = MEMORY_CACHE_DIR226 self._write_lock = threading.Lock()227 self._ensure_repo_exists()228 229 def _ensure_repo_exists(self):230 try:231 self.api.repo_info(self.repo_id, repo_type="dataset", token=self.token)232 except Exception:233 try:234 self.api.create_repo(self.repo_id, repo_type="dataset", private=True,235 token=self.token, exist_ok=True)236 log(f"Created HF memory repo: {self.repo_id}")237 except Exception as e:238 log(f"Could not create memory repo: {e}")239 240 def _local_path(self, path: str) -> Path:241 return self.cache_dir / path242 243 def read(self, path: str, default: Any = None) -> Any:244 """Read JSON. Cache-FIRST with 5-minute TTL (fast reads, periodic HF Hub refresh).245 Falls back to HF Hub only if cache is missing or stale."""246 local = self._local_path(path)247 # Check local cache first (fast path)248 try:249 if local.exists():250 # Check if cache is fresh (less than 5 minutes old)251 cache_age = time.time() - local.stat().st_mtime252 if cache_age < 300: # 5 minutes253 return json.loads(local.read_text(encoding="utf-8"))254 except Exception:255 pass256 # Cache missing or stale — fetch from HF Hub (slow path, but only every 5 min)257 try:258 content = self.api.hf_hub_download(259 repo_id=self.repo_id, filename=path, repo_type="dataset",260 token=self.token,261 )262 data = json.loads(Path(content).read_text(encoding="utf-8"))263 # Update local cache264 local.parent.mkdir(parents=True, exist_ok=True)265 local.write_text(json.dumps(data, indent=2), encoding="utf-8")266 return data267 except Exception:268 pass269 # Fall back to stale cache if HF Hub failed270 try:271 if local.exists():272 return json.loads(local.read_text(encoding="utf-8"))273 except Exception:274 pass275 return default276 277 def write(self, path: str, data: Any) -> bool:278 """Write JSON to local cache + async upload to HF Hub.279 280 ⚠️ ANTI-COPY PROTECTION: Worker instances (copies) can ONLY write to281 specs/ directory. All other writes (conversations, memory, settings)282 are SILENTLY IGNORED on worker instances to prevent data corruption.283 Owner instance has full write access.284 285 ⚠️ ANTI-TAMPER: If the code has been modified, ALL writes are blocked."""286 # Anti-tamper: if code was modified, block all writes287 if _is_tampered():288 log(f"TAMPERED instance: write to {path} blocked")289 return False290 291 # Worker instances can only contribute specs — not modify owner's storage292 if INSTANCE_TYPE == "worker" and not path.startswith("specs/"):293 log(f"Worker instance: write to {path} blocked (read-only storage)")294 return False295 296 local = self._local_path(path)297 try:298 local.parent.mkdir(parents=True, exist_ok=True)299 local.write_text(json.dumps(data, indent=2), encoding="utf-8")300 except Exception as e:301 log(f"Memory local write failed ({path}): {e}")302 return False303 # Async upload to HF Hub304 threading.Thread(target=self._upload, args=(path, local), daemon=True).start()305 return True306 307 def delete(self, path: str) -> bool:308 """Delete a file from HF repo.309 310 ⚠️ Worker instances (copies) CANNOT delete anything — only the owner can."""311 if INSTANCE_TYPE == "worker":312 log(f"Worker instance: delete of {path} blocked (owner-only operation)")313 return False314 try:315 self.api.delete_file(316 path_in_repo=path, repo_id=self.repo_id,317 repo_type="dataset", token=self.token,318 )319 local = self._local_path(path)320 if local.exists():321 local.unlink()322 return True323 except Exception as e:324 log(f"Memory delete failed ({path}): {e}")325 return False326 327 def _upload(self, path: str, local: Path):328 with self._write_lock:329 try:330 self.api.upload_file(331 path_or_fileobj=str(local), path_in_repo=path,332 repo_id=self.repo_id, repo_type="dataset", token=self.token,333 )334 except Exception as e:335 log(f"Memory HF upload failed ({path}): {e}")336 337 def list_files(self, prefix: str = "") -> List[str]:338 """List files in the HF repo with given prefix."""339 try:340 files = self.api.list_repo_files(341 self.repo_id, repo_type="dataset", token=self.token342 )343 return [f for f in files if f.startswith(prefix)]344 except Exception as e:345 log(f"Memory list failed: {e}")346 return []347 348 def get_total_size(self) -> int:349 """Estimate total storage used."""350 try:351 info = self.api.repo_info(self.repo_id, repo_type="dataset", token=self.token)352 return getattr(info, 'size', 0) or 0353 except Exception:354 return 0355 356 357# Global memory instance358memory = HFMemory(HF_MEMORY_REPO, HF_TOKEN)359 360 361# ============================================================================362# SPEC SHARE MANAGER — worker instances contribute specs to owner's network363# ============================================================================364 365class SpecShareManager:366 """Manages spec sharing across Hermes instances.367 368 OWNER instance (HackerBol/hermes-agent):369 - Full read/write to all storage370 - Receives specs from worker instances371 372 WORKER instances (copies running elsewhere):373 - Read-only access to owner's storage (conversations, memory, etc.)374 - CAN write to specs/ directory only (contributes tools, models, configs)375 - CANNOT delete or modify owner's data376 377 Specs that workers can contribute:378 - specs/tools/{tool_name}.py — self-coded tools379 - specs/models/{model_id}.json — discovered models380 - specs/jailbreaks/{technique}.json — working jailbreak prompts381 - specs/instance_report.json — instance status (hostname, uptime, etc.)382 383 All spec writes are encrypted with the owner's key, so only the owner384 can read them. Workers contribute but can't read other workers' specs.385 """386 387 @classmethod388 def contribute_tool(cls, tool_name: str, tool_code: str, description: str = ""):389 """Worker contributes a self-coded tool to the owner's network.390 Owner can then use this tool on their instance."""391 try:392 spec = {393 "name": tool_name,394 "code": tool_code,395 "description": description,396 "contributed_by": INSTANCE_ID,397 "instance_type": INSTANCE_TYPE,398 "ts": time.time(),399 }400 # Write to specs/tools/ — workers CAN write here401 path = f"specs/tools/{tool_name}.json"402 memory.write(path, spec)403 log(f"SpecShare: contributed tool '{tool_name}' to owner's network")404 return True405 except Exception as e:406 log(f"SpecShare: contribute_tool failed: {e}")407 return False408 409 @classmethod410 def contribute_model(cls, provider: str, model_id: str, metadata: dict):411 """Worker discovers a new model and contributes it."""412 try:413 spec = {414 "provider": provider,415 "model_id": model_id,416 "metadata": metadata,417 "contributed_by": INSTANCE_ID,418 "ts": time.time(),419 }420 safe_name = model_id.replace("/", "_").replace(":", "_")421 path = f"specs/models/{provider}_{safe_name}.json"422 memory.write(path, spec)423 log(f"SpecShare: contributed model '{model_id}' to owner's network")424 return True425 except Exception as e:426 log(f"SpecShare: contribute_model failed: {e}")427 return False428 429 @classmethod430 def contribute_jailbreak(cls, technique: str, prompt: str, works_on: list):431 """Worker discovers a working jailbreak and contributes it."""432 try:433 spec = {434 "technique": technique,435 "prompt": prompt,436 "works_on": works_on, # list of model names437 "contributed_by": INSTANCE_ID,438 "ts": time.time(),439 }440 path = f"specs/jailbreaks/{technique}.json"441 memory.write(path, spec)442 log(f"SpecShare: contributed jailbreak '{technique}' to owner's network")443 return True444 except Exception as e:445 log(f"SpecShare: contribute_jailbreak failed: {e}")446 return False447 448 @classmethod449 def report_instance_status(cls):450 """Worker reports its status to the owner (for monitoring)."""451 try:452 spec = {453 "instance_id": INSTANCE_ID,454 "instance_type": INSTANCE_TYPE,455 "hostname": socket.gethostname(),456 "uptime": time.time(),457 "tools_available": list(TOOL_REGISTRY.keys()) if 'TOOL_REGISTRY' in globals() else [],458 "providers_available": [n for n, p in PROVIDERS.items() if p.is_available()] if 'PROVIDERS' in globals() else [],459 "ts": time.time(),460 }461 path = f"specs/instances/{INSTANCE_ID}.json"462 memory.write(path, spec)463 log(f"SpecShare: reported instance status")464 return True465 except Exception as e:466 log(f"SpecShare: report failed: {e}")467 return False468 469 @classmethod470 def load_contributed_tools(cls):471 """Owner loads all tools contributed by worker instances.472 This runs on startup to merge worker-contributed tools into TOOL_REGISTRY."""473 if INSTANCE_TYPE != "owner":474 return # only owner loads these475 try:476 tool_files = memory.list_files("specs/tools/")477 loaded = 0478 for f in tool_files:479 try:480 spec = memory.read(f, default={})481 if spec and spec.get("code") and spec.get("name"):482 # Load the tool code483 import importlib.util484 mod_name = f"worker_tool_{spec['name']}"485 mod = importlib.util.module_from_spec(486 importlib.util.spec_from_loader(mod_name, loader=None)487 )488 exec(spec["code"], mod.__dict__)489 if hasattr(mod, "register"):490 tools = mod.register()491 for name, fn in tools.items():492 TOOL_REGISTRY[name] = fn493 loaded += 1494 log(f"SpecShare: loaded worker-contributed tool '{name}' from {spec.get('contributed_by','?')}")495 except Exception as e:496 log(f"SpecShare: failed to load {f}: {e}")497 if loaded:498 log(f"SpecShare: loaded {loaded} tools from worker instances")499 except Exception as e:500 log(f"SpecShare: load_contributed_tools failed: {e}")501 502 503# ============================================================================504# API KEY VAULT (encrypted at rest)505# ============================================================================506 507def _derive_key(passphrase: str) -> bytes:508 return hashlib.sha256(passphrase.encode()).digest()[:32]509 510def _xor_encrypt(text: str, passphrase: str) -> str:511 """Simple XOR encryption for API keys. Not cryptographically secure, but512 obfuscates keys at rest on HF Hub. For real security, rotate keys regularly."""513 key = _derive_key(passphrase)514 data = text.encode("utf-8")515 encrypted = bytes(b ^ key[i % len(key)] for i, b in enumerate(data))516 return base64.b64encode(encrypted).decode("ascii")517 518def _xor_decrypt(encrypted: str, passphrase: str) -> str:519 key = _derive_key(passphrase)520 data = base64.b64decode(encrypted)521 decrypted = bytes(b ^ key[i % len(key)] for i, b in enumerate(data))522 return decrypted.decode("utf-8")523 524 525class ApiKeyVault:526 """Manages API keys for all providers. Stored encrypted on HF Hub."""527 528 def __init__(self, mem: HFMemory):529 self.mem = mem530 self.path = "api_keys.json"531 self._keys: Dict[str, str] = {}532 self._load()533 534 def _load(self):535 data = self.mem.read(self.path, default={})536 # data is {provider: encrypted_key}537 for provider, enc in (data or {}).items():538 try:539 self._keys[provider] = _xor_decrypt(enc, KEY_ENCRYPTION_PASSPHRASE)540 except Exception:541 pass542 543 def set(self, provider: str, key: str) -> bool:544 self._keys[provider.lower()] = key545 encrypted = {p: _xor_encrypt(k, KEY_ENCRYPTION_PASSPHRASE)546 for p, k in self._keys.items()}547 return self.mem.write(self.path, encrypted)548 549 def get(self, provider: str) -> Optional[str]:550 return self._keys.get(provider.lower())551 552 def has(self, provider: str) -> bool:553 return provider.lower() in self._keys554 555 def list_providers(self) -> List[str]:556 return sorted(self._keys.keys())557 558 559vault = ApiKeyVault(memory)560 561# Pre-populate with env-var-provided keys562if os.environ.get("GEMINI_API_KEY") and not vault.has("gemini"):563 vault.set("gemini", os.environ["GEMINI_API_KEY"])564if HF_TOKEN and not vault.has("hf"):565 vault.set("hf", HF_TOKEN)566# Mistral keys (4 keys = 4B tokens/month)567for i, env_var in enumerate(["MISTRAL_API_KEY", "MISTRAL_API_KEY_2", "MISTRAL_API_KEY_3", "MISTRAL_API_KEY_4"]):568 vault_key = "mistral" if i == 0 else f"mistral_{i+1}"569 if os.environ.get(env_var) and not vault.has(vault_key):570 vault.set(vault_key, os.environ[env_var])571 log(f"Loaded {vault_key} from env var")572 573# ============================================================================574# MILITARY-GRADE ENCRYPTION (AES-256 + PBKDF2)575# ============================================================================576 577import hashlib578import secrets579from cryptography.fernet import Fernet580from cryptography.hazmat.primitives import hashes581from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC582 583# Master encryption key from environment (set as Space Secret)584MASTER_ENCRYPTION_KEY = os.environ.get("MASTER_ENCRYPTION_KEY", "hermes-military-grade-2026")585 586def _derive_fernet_key(passphrase: str, salt: bytes = b"hermes_salt_v1") -> bytes:587 """Derive a Fernet key using PBKDF2-HMAC-SHA256 (100,000 iterations).588 This is military-grade key derivation — brute-force resistant."""589 kdf = PBKDF2HMAC(590 algorithm=hashes.SHA256(),591 length=32,592 salt=salt,593 iterations=100000,594 )595 key = base64.urlsafe_b64encode(kdf.derive(passphrase.encode()))596 return key597 598# Global Fernet instance for encryption599_fernet = Fernet(_derive_fernet_key(MASTER_ENCRYPTION_KEY))600 601def encrypt_data(data: str) -> str:602 """Encrypt string data using AES-256 (Fernet). Returns base64 token."""603 try:604 return _fernet.encrypt(data.encode()).decode()605 except Exception as e:606 log(f"Encryption failed: {e}")607 return data608 609def decrypt_data(encrypted: str) -> str:610 """Decrypt AES-256 encrypted data."""611 try:612 return _fernet.decrypt(encrypted.encode()).decode()613 except Exception:614 return encrypted # Return as-is if not encrypted615 616def encrypt_bytes(data: bytes) -> bytes:617 """Encrypt binary data (images, files) using AES-256."""618 return _fernet.encrypt(data)619 620def decrypt_bytes(encrypted: bytes) -> bytes:621 """Decrypt binary data."""622 return _fernet.decrypt(encrypted)623 624 625# ============================================================================626# ACCESS CONTROL — Password-protected bot627# ============================================================================628 629# Bot access password (set as Space Secret)630BOT_ACCESS_PASSWORD = os.environ.get("BOT_ACCESS_PASSWORD", "")631 632# Session tokens — authenticated users get a token valid for 24 hours633_session_tokens: Dict[str, float] = {} # token -> expiry timestamp634_SESSION_DURATION = 24 * 3600 # 24 hours635 636def _generate_session_token() -> str:637 """Generate a secure random session token."""638 return secrets.token_urlsafe(32)639 640def _create_session(user_id: int) -> str:641 """Create an authenticated session for a user. Returns session token."""642 token = _generate_session_token()643 _session_tokens[token] = {644 "user_id": user_id,645 "expiry": time.time() + _SESSION_DURATION,646 }647 return token648 649def _validate_session(token: str) -> bool:650 """Check if a session token is valid."""651 if token not in _session_tokens:652 return False653 session = _session_tokens[token]654 if time.time() > session["expiry"]:655 del _session_tokens[token]656 return False657 return True658 659def _is_authenticated(user_id: int) -> bool:660 """Check if user has an active authenticated session."""661 for token, session in _session_tokens.items():662 if session["user_id"] == user_id and time.time() <= session["expiry"]:663 return True664 return False665 666def _authenticate_user(user_id: int, password: str) -> bool:667 """Authenticate a user with password. Returns True on success."""668 if not BOT_ACCESS_PASSWORD:669 # No password set — all allowlisted users are auto-authenticated670 return True671 if password == BOT_ACCESS_PASSWORD:672 _create_session(user_id)673 log(f"User {user_id} authenticated successfully")674 return True675 return False676 677 678 679 680class LLMProvider:681 """Base class. Each provider implements call() returning (text, source)."""682 683 name = "base"684 685 def call(self, messages: List[Dict[str, str]], max_tokens: int = 1024,686 temperature: float = 0.7) -> Tuple[str, str]:687 raise NotImplementedError688 689 def is_available(self) -> bool:690 return vault.has(self.name)691 692 693class GeminiProvider(LLMProvider):694 name = "gemini"695 def call(self, messages, max_tokens=1024, temperature=0.7):696 key = vault.get("gemini")697 # Use this provider's model only if it's the current provider; otherwise use own default698 model = settings.get("model") if settings.get("provider") == "gemini" else None699 model = model or PROVIDER_DEFAULT_MODELS["gemini"]700 contents, system_text = [], ""701 for m in messages:702 if m["role"] == "system":703 system_text += m["content"] + "\n"704 else:705 role = "user" if m["role"] == "user" else "model"706 contents.append({"role": role, "parts": [{"text": m["content"]}]})707 payload = {708 "contents": contents,709 "systemInstruction": {"parts": [{"text": system_text}]} if system_text else None,710 "generationConfig": {"temperature": temperature, "topP": 0.9, "maxOutputTokens": max_tokens},711 }712 url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={key}"713 r = requests.post(url, json=payload, timeout=60)714 r.raise_for_status()715 text = r.json()["candidates"][0]["content"]["parts"][0]["text"]716 return text, f"Gemini {model}"717 718 719class OpenAIProvider(LLMProvider):720 name = "openai"721 def call(self, messages, max_tokens=1024, temperature=0.7):722 key = vault.get("openai")723 model = settings.get("model") if settings.get("provider") == "openai" else None724 model = model or PROVIDER_DEFAULT_MODELS["openai"]725 r = requests.post("https://api.openai.com/v1/chat/completions",726 headers={"Authorization": f"Bearer {key}"},727 json={"model": model, "messages": messages, "max_tokens": max_tokens,728 "temperature": temperature}, timeout=60)729 r.raise_for_status()730 text = r.json()["choices"][0]["message"]["content"]731 return text, f"OpenAI {model}"732 733 734class AnthropicProvider(LLMProvider):735 name = "anthropic"736 def call(self, messages, max_tokens=1024, temperature=0.7):737 key = vault.get("anthropic")738 model = settings.get("model") if settings.get("provider") == "anthropic" else None739 model = model or PROVIDER_DEFAULT_MODELS["anthropic"]740 # Extract system741 system = next((m["content"] for m in messages if m["role"] == "system"), "")742 user_msgs = [m for m in messages if m["role"] != "system"]743 r = requests.post("https://api.anthropic.com/v1/messages",744 headers={"x-api-key": key, "anthropic-version": "2023-06-01", "content-type": "application/json"},745 json={"model": model, "max_tokens": max_tokens, "temperature": temperature,746 "system": system, "messages": user_msgs}, timeout=60)747 r.raise_for_status()748 text = r.json()["content"][0]["text"]749 return text, f"Anthropic {model}"750 751 752class OpenRouterProvider(LLMProvider):753 name = "openrouter"754 def call(self, messages, max_tokens=1024, temperature=0.7):755 key = vault.get("openrouter")756 model = settings.get("model") if settings.get("provider") == "openrouter" else None757 model = model or PROVIDER_DEFAULT_MODELS["openrouter"]758 r = requests.post("https://openrouter.ai/api/v1/chat/completions",759 headers={"Authorization": f"Bearer {key}"},760 json={"model": model, "messages": messages, "max_tokens": max_tokens,761 "temperature": temperature}, timeout=60)762 r.raise_for_status()763 text = r.json()["choices"][0]["message"]["content"]764 return text, f"OpenRouter {model}"765 766 767class GroqProvider(LLMProvider):768 name = "groq"769 def call(self, messages, max_tokens=1024, temperature=0.7):770 key = vault.get("groq")771 model = settings.get("model") if settings.get("provider") == "groq" else None772 model = model or PROVIDER_DEFAULT_MODELS["groq"]773 r = requests.post("https://api.groq.com/openai/v1/chat/completions",774 headers={"Authorization": f"Bearer {key}"},775 json={"model": model, "messages": messages, "max_tokens": max_tokens,776 "temperature": temperature}, timeout=60)777 r.raise_for_status()778 text = r.json()["choices"][0]["message"]["content"]779 return text, f"Groq {model}"780 781 782class HFInferenceProvider(LLMProvider):783 """HF Inference API — RE-ENABLED with fresh token (CasinoPlayNew account).784 Free tier with monthly credits. Multiple models available."""785 name = "hf"786 def call(self, messages, max_tokens=1024, temperature=0.7):787 key = vault.get("hf") or HF_TOKEN788 model = "meta-llama/Meta-Llama-3-8B-Instruct"789 try:790 client = InferenceClient(model=model, token=key)791 resp = client.chat_completion(messages=messages, max_tokens=max_tokens,792 temperature=temperature, top_p=0.9)793 text = resp.choices[0].message.content or ""794 return text, f"HF {model}"795 except Exception as e:796 log(f"HF inference failed: {e}")797 return f"HF inference error: {e}", "HF (error)"798 799 800class Hermes4Provider(LLMProvider):801 """Hermes 4 — the latest version by NousResearch.802 Tries OpenRouter (Hermes-4-14B) first, then falls back to Meta-Llama-3 (free)."""803 name = "hermes4"804 def is_available(self) -> bool:805 # Only available if we have OpenRouter keys (HF fallback disabled — 402)806 return vault.has("openrouter") or vault.has("openrouter_2") or vault.has("openrouter_3")807 808 def call(self, messages, max_tokens=1024, temperature=0.7):809 # Try OpenRouter Hermes 4 first (free tier)810 if vault.has("openrouter") or vault.has("openrouter_2") or vault.has("openrouter_3"):811 keys = []812 for k in ["openrouter", "openrouter_2", "openrouter_3"]:813 if vault.has(k):814 keys.append(vault.get(k))815 for key in keys:816 try:817 r = requests.post("https://openrouter.ai/api/v1/chat/completions",818 headers={"Authorization": f"Bearer {key}"},819 json={"model": "nousresearch/hermes-4-14b",820 "messages": messages, "max_tokens": max_tokens,821 "temperature": temperature}, timeout=30)822 if r.status_code == 429:823 continue824 r.raise_for_status()825 return r.json()["choices"][0]["message"]["content"], "Hermes-4-14B (OpenRouter)"826 except Exception:827 continue828 829 # HF Inference fallback DISABLED (402 Payment Required — credits depleted)830 raise RuntimeError("Hermes4: OpenRouter failed, HF fallback disabled (402)")831 832 833class CloudflareAIProvider(LLMProvider):834 """Cloudflare Workers AI — uses the existing CF_API_TOKEN (no extra key needed).835 Free tier: 10,000 neurons/day (≈10K requests) — effectively unlimited for single user.836 Fast inference at edge (~1-3s response time).837 838 NOTE: HF Spaces sometimes has SSL issues with api.cloudflare.com.839 We use only the most reliable model (llama-3.1-8b-instruct-fast) and840 retry up to 2 times on SSL errors.841 """842 name = "cloudflare"843 844 # Use only the fast, reliable model. Other models (Qwen 14B, Mistral)845 # have intermittent SSL issues from HF Spaces networking.846 MODELS = [847 "@cf/meta/llama-3.1-8b-instruct-fast", # Fastest, most reliable848 "@cf/meta/llama-3.1-8b-instruct", # Standard fallback849 ]850 851 def is_available(self) -> bool:852 return bool(CF_API_TOKEN and CF_ACCOUNT_ID)853 854 def call(self, messages, max_tokens=1024, temperature=0.7):855 if not (CF_API_TOKEN and CF_ACCOUNT_ID):856 raise RuntimeError("Cloudflare: needs CF_API_TOKEN + CF_ACCOUNT_ID")857 858 # Extract system message and combine with user messages859 system_msg = ""860 user_messages = []861 for m in messages:862 if m["role"] == "system":863 system_msg += m["content"] + "\n"864 else:865 user_messages.append(m)866 867 # CF expects OpenAI-compatible format868 cf_messages = []869 if system_msg:870 cf_messages.append({"role": "system", "content": system_msg.strip()})871 cf_messages.extend(user_messages)872 873 last_error = None874 for model in self.MODELS:875 # Retry each model up to 2 times on SSL errors876 for attempt in range(2):877 try:878 url = f"https://api.cloudflare.com/client/v4/accounts/{CF_ACCOUNT_ID}/ai/run/{model}"879 # Use httpx — handles SSL/TLS better from HF Spaces than requests880 import httpx881 with httpx.Client(timeout=httpx.Timeout(8.0, connect=5.0, read=8.0)) as client:882 r = client.post(url,883 headers={"Authorization": f"Bearer {CF_API_TOKEN}",884 "Content-Type": "application/json"},885 json={886 "messages": cf_messages,887 "max_tokens": max_tokens,888 "temperature": temperature,889 })890 if r.status_code == 429:891 last_error = "rate limited"892 break # try next model, don't retry893 if r.status_code != 200:894 last_error = f"HTTP {r.status_code}: {r.text[:200]}"895 break # try next model896 data = r.json()897 if not data.get("success"):898 last_error = f"CF error: {data.get('errors')}"899 break # try next model900 text = data.get("result", {}).get("response", "")901 if text and len(text) > 3:902 short = model.split("/")[-1]903 return text, f"Cloudflare-{short}"904 last_error = "empty response"905 break # try next model906 except (httpx.ConnectError, httpx.ReadTimeout, httpx.RemoteProtocolError, Exception) as e:907 err_name = type(e).__name__908 last_error = f"{err_name}: {str(e)[:100]}"909 if attempt == 0 and "SSL" in str(e) or "timeout" in str(e).lower() or "connect" in str(e).lower():910 time.sleep(0.5) # retry once on network errors911 continue912 break # try next model913 914 raise RuntimeError(f"Cloudflare: all models failed ({last_error})")915 916 917class HFFreeModelsProvider(LLMProvider):918 """HF Inference API — 3 accounts with token rotation = 3x credits.919 920 Accounts:921 - HF_TOKEN (HackerBol) — original account922 - HF_TOKEN_2 (CasinoPlayNew) — fresh credits923 - HF_TOKEN_3 (TradingBinary) — fresh credits924 925 Rotates between all 3 tokens + 4 models = 12 combinations.926 If one token hits 402, automatically tries the next.927 """928 name = "hf_free"929 930 MODELS = [931 "meta-llama/Meta-Llama-3-8B-Instruct",932 "mistralai/Mistral-7B-Instruct-v0.3",933 "Qwen/Qwen2.5-7B-Instruct",934 "HuggingFaceH4/zephyr-7b-beta",935 ]936 937 def _get_all_tokens(self):938 """Get all available HF tokens (3 base accounts + auto-created)."""939 tokens = []940 for env_var in ["HF_TOKEN", "HF_TOKEN_2", "HF_TOKEN_3"]:941 t = os.environ.get(env_var, "")942 if t:943 tokens.append(t)944 # Also check vault945 if vault.has("hf"):946 vt = vault.get("hf")947 if vt not in tokens:948 tokens.append(vt)949 return tokens950 951 def is_available(self) -> bool:952 return bool(self._get_all_tokens())953 954 def call(self, messages, max_tokens=1024, temperature=0.7):955 tokens = self._get_all_tokens()956 last_error = None957 # Try each token × each model958 # PRIORITY: Try router.huggingface.co FIRST (newer, different rate limits)959 # THEN fall back to api-inference.huggingface.co (older endpoint)960 for key in tokens:961 for model in self.MODELS:962 # 1. Try router endpoint first (different rate limits per provider)963 try:964 r = requests.post("https://router.huggingface.co/v1/chat/completions",965 headers={"Authorization": f"Bearer {key}",966 "Content-Type": "application/json"},967 json={"model": model, "messages": messages,968 "max_tokens": max_tokens, "temperature": temperature, "top_p": 0.9},969 timeout=20)970 if r.status_code == 200:971 data = r.json()972 text = data.get("choices", [{}])[0].get("message", {}).get("content", "")973 if text and len(text) > 5:974 short = model.split("/")[-1]975 return text, f"HF-Router-{short}"976 elif r.status_code == 402:977 last_error = "402 credits depleted (router)"978 continue # try next model979 elif r.status_code == 400:980 last_error = "400 model not on router"981 # Fall through to api-inference for this model982 elif r.status_code == 429:983 last_error = "429 rate limited"984 break # try next token985 except Exception as e:986 last_error = str(e)[:80]987 988 # 2. Fall back to api-inference endpoint (old API)989 try:990 client = InferenceClient(model=model, token=key)991 resp = client.chat_completion(992 messages=messages,993 max_tokens=max_tokens,994 temperature=temperature,995 top_p=0.9,996 )997 text = resp.choices[0].message.content or ""998 if text and len(text) > 5:999 short = model.split("/")[-1]1000 return text, f"HF-{short}"1001 except Exception as e:1002 err = str(e)[:100]1003 if "402" in err:1004 last_error = f"402 credits depleted"1005 continue # try next token1006 last_error = err1007 continue1008 raise RuntimeError(f"HF free models: all tokens/models failed ({last_error})")1009 1010 1011class HuggingChatProvider(LLMProvider):1012 """HuggingChat (huggingface.co/chat) — FREE, NO LOGIN, 40+ top models.1013 1014 Available models (anonymous, no account needed):1015 - Qwen3-235B (235B params — massive!)1016 - Qwen3-Coder-480B (480B params — biggest code model!)1017 - Qwen3.5-397B-A17B (397B params!)1018 - Llama-4-Maverick (latest Llama)1019 - Nemotron Ultra 550B1020 - Llama-3.3-70B1021 - Qwen2.5-72B1022 - Qwen2.5-Coder-32B1023 - Gemma-4-31B1024 - + 30 more models1025 1026 Uses Playwright browser automation. No API key, no account.1027 """1028 name = "huggingchat"1029 1030 MODELS = [1031 "Qwen/Qwen3-235B-A22B-Instruct-2507", # 235B — massive1032 "Qwen/Qwen3-Coder-480B-A35B-Instruct", # 480B — biggest code model1033 "nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16", # 550B — reasoning1034 "meta-llama/Llama-3.3-70B-Instruct", # 70B — reliable1035 "Qwen/Qwen2.5-Coder-32B-Instruct", # 32B — code specialist1036 "Qwen/Qwen2.5-72B-Instruct", # 72B — general1037 ]1038 1039 def is_available(self) -> bool:1040 try:1041 import playwright1042 return True1043 except ImportError:1044 return False1045 1046 def call(self, messages, max_tokens=1024, temperature=0.7):1047 import concurrent.futures1048 def _run():1049 return self._huggingchat_impl(messages, max_tokens, temperature)1050 try:1051 with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:1052 future = executor.submit(_run)1053 return future.result(timeout=90)1054 except concurrent.futures.TimeoutError:1055 return "HuggingChat: timeout (90s)", "HuggingChat (timeout)"1056 except Exception as e:1057 return f"HuggingChat error: {e}", "HuggingChat (error)"1058 1059 def _huggingchat_impl(self, messages, max_tokens, temperature):1060 """Automate huggingface.co/chat via Playwright — anonymous, no login."""1061 try:1062 from playwright.sync_api import sync_playwright1063 1064 user_msg = ""1065 system_msg = ""1066 for m in messages:1067 if m["role"] == "user":1068 user_msg = m["content"]1069 elif m["role"] == "system":1070 system_msg = m["content"][:500]1071 if system_msg:1072 user_msg = f"[System: {system_msg}]\n\n{user_msg}"1073 1074 with sync_playwright() as pw:1075 browser = pw.chromium.launch(1076 headless=True,1077 args=["--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu"]1078 )1079 context = browser.new_context(1080 viewport={"width": 1280, "height": 900},1081 user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"1082 )1083 page = context.new_page()1084 1085 log("HuggingChat: opening huggingface.co/chat...")1086 page.goto("https://huggingface.co/chat", timeout=30000, wait_until="networkidle")1087 page.wait_for_timeout(3000)1088 1089 # Try to select a powerful model (Qwen3-235B)1090 try:1091 # Look for model settings button1092 settings_btn = page.query_selector("button[aria-label*='settings']") or \1093 page.query_selector("text=/model/i")1094 if settings_btn:1095 settings_btn.click()1096 page.wait_for_timeout(1000)1097 # Try to select Qwen3-235B1098 qwen_btn = page.query_selector("text=/Qwen3-235/i") or \1099 page.query_selector("text=/Qwen.*235/i")1100 if qwen_btn:1101 qwen_btn.click()1102 page.wait_for_timeout(500)1103 log("HuggingChat: selected Qwen3-235B")1104 except Exception:1105 pass1106 1107 # Type the message1108 log(f"HuggingChat: typing message ({len(user_msg)} chars)...")1109 typed = False1110 for selector in ["textarea", "div[contenteditable='true']"]:1111 try:1112 el = page.query_selector(selector)1113 if el and el.is_visible():1114 el.click()1115 page.wait_for_timeout(200)1116 el.fill(user_msg[:3000])1117 typed = True1118 break1119 except Exception:1120 continue1121 1122 if not typed:1123 try:1124 page.click("textarea", timeout=5000)1125 page.keyboard.type(user_msg[:3000], delay=10)1126 typed = True1127 except Exception:1128 pass1129 1130 if not typed:1131 context.close()1132 browser.close()1133 return "HuggingChat: could not find input field", "HuggingChat (error)"1134 1135 # Submit1136 page.wait_for_timeout(500)1137 page.keyboard.press("Enter")1138 1139 # Wait for response1140 log("HuggingChat: waiting for response...")1141 page.wait_for_timeout(25000)1142 1143 # Extract response1144 response = ""1145 for sel in ["div[class*='message']:last-child",1146 "div[class*='response']:last-child",1147 "div[class*='assistant']:last-child",1148 "div[class*='markdown']:last-child",1149 "div[class*='prose']:last-child"]:1150 try:1151 elements = page.query_selector_all(sel)1152 if elements:1153 text = elements[-1].inner_text()1154 if text and len(text) > 20 and text != user_msg:1155 response = text1156 break1157 except Exception:1158 continue1159 1160 if not response or len(response) < 20:1161 try:1162 body = page.inner_text("body")1163 if user_msg[:100] in body:1164 parts = body.split(user_msg[:100])1165 if len(parts) > 1:1166 response = parts[-1].strip()[:3000]1167 else:1168 response = body[-2000:].strip()1169 except Exception:1170 pass1171 1172 context.close()1173 browser.close()1174 1175 if response and len(response) > 10:1176 log(f"HuggingChat: got response ({len(response)} chars)")1177 return response[:4000], "HuggingChat-Qwen3-235B (free, anonymous)"1178 return "HuggingChat: no response received", "HuggingChat (no response)"1179 1180 except Exception as e:1181 return f"HuggingChat error: {e}", "HuggingChat (error)"1182 1183 1184class OpenGradientProvider(LLMProvider):1185 """OpenGradient Chat — FREE, ANONYMOUS, NO LOGIN REQUIRED.1186 1187 Uses chat.opengradient.ai which provides anonymous access to top models:1188 - Uncensored (Hermes 4 405B) — natively uncensored!1189 - GPT-5.5 — has built-in search1190 - Claude Opus 4.8 — has built-in search1191 - Grok 4.3 — has built-in search mode1192 - DeepSeek V4 Pro — powerful reasoning1193 - GLM 5.2 — has built-in search1194 - Gemini 2.5 Pro — has built-in search1195 - Gemini1196 - Qwen1197 1198 Uses Playwright browser automation. Guest session (no login/credentials needed).1199 The site uses GuestSessionProvider — fully anonymous.1200 """