#!/usr/bin/env python3 """ [OPS] Sovereign Skill Installer Part of the Gemini Sovereign Infrastructure Suite. Packages and installs Hermes skills onto remote wizard nodes. """ import os import sys import argparse import subprocess from pathlib import Path #!/usr/bin/env python3 """ [OPS] Sovereign Skill Installer Part of the Gemini Sovereign Infrastructure Suite. Packages and installs Hermes skills onto remote wizard nodes. """ import os import sys import argparse import subprocess from pathlib import Path import yaml # --- CONFIGURATION --- # Load fleet inventory and path contracts from central timmy-config def get_config_path(): return os.environ.get('TIMMY_CONFIG') or os.path.join( os.path.dirname(os.path.abspath(__file__)), '..', 'config.yaml' ) def load_fleet_inventory(): """Return dict of {{host: {{ip, port, role, remote_root, capabilities}}}}""" try: with open(get_config_path(), 'r') as f: cfg = yaml.safe_load(f) inv = cfg.get('fleet', {}).get('inventory', {}) return inv if inv else {} except Exception: return {} def get_path_contract(key, default): """Return path contract value from config.""" try: with open(get_config_path(), 'r') as f: cfg = yaml.safe_load(f) return cfg.get('fleet', {}).get('path_contracts', {}).get(key, default) except Exception: return default FLEET = load_fleet_inventory() HERMES_ROOT = get_path_contract('hermes_agent_local', '../hermes-agent') REMOTE_ROOT = get_path_contract('hermes_remote', '/opt/hermes') SKILLS_DIR = "skills" class SkillInstaller: def __init__(self, host: str, ip: str): self.host = host self.ip = ip self.hermes_path = Path(HERMES_ROOT).expanduser().resolve() def log(self, message: str): print(f"[*] {message}") def error(self, message: str): print(f"[!] ERROR: {message}") sys.exit(1) def install_skill(self, skill_name: str): self.log(f"Installing skill '{skill_name}' to {self.host} ({self.ip})...") skill_path = self.hermes_path / SKILLS_DIR / skill_name if not skill_path.exists(): self.error(f"Skill '{skill_name}' not found in {skill_path}") # 1. Compress skill self.log("Compressing skill...") tar_file = f"{skill_name}.tar.gz" subprocess.run(["tar", "-czf", tar_file, "-C", str(skill_path.parent), skill_name]) # 2. Upload to remote self.log("Uploading to remote...") remote_path = f"{REMOTE_ROOT}/skills/{skill_name}" subprocess.run(["ssh", f"root@{self.ip}", f"mkdir -p {REMOTE_ROOT}/skills"]) subprocess.run(["scp", tar_file, f"root@{self.ip}:/tmp/"]) # 3. Extract and register self.log("Extracting and registering...") extract_cmd = f"tar -xzf /tmp/{tar_file} -C {REMOTE_ROOT}/skills/ && rm /tmp/{tar_file}" subprocess.run(["ssh", f"root@{self.ip}", extract_cmd]) # Registration logic (simplified) # In a real scenario, we'd update the wizard's config.yaml self.log(f"[SUCCESS] Skill '{skill_name}' installed on {self.host}") # Cleanup local tar os.remove(tar_file) def main(): parser = argparse.ArgumentParser(description="Gemini Skill Installer") parser.add_argument("host", help="Target host name") parser.add_argument("ip", help="Target host IP") parser.add_argument("skill", help="Skill name to install") args = parser.parse_args() installer = SkillInstaller(args.host, args.ip) installer.install_skill(args.skill) if __name__ == "__main__": main()