Files

1 line
17 KiB
JSON

{"repo": "badtechresearch/santana", "files": {"tools/dev_exec.py": {"1d183280aef655ada8b21af4f99ba6076392f199": {"t": 1785745437.9306436, "n": 7616, "h": "20426b6a2408680f", "c": "\"\"\"\ndev_exec.py — Exécution libre de commandes shell pour Santana.\n\nL'outil qui manquait à Santana pour coder librement.\nPas d'allowlist, pas de denylist, pas de sandbox mount.\n\nGarde-fous systémiques uniquement :\n1. Timeout + kill forcé\n2. Filtre post-output (patterns de tokens/secrets masqués)\n3. Backup des fichiers critiques avant exécution\n4. Snapshot git avant commandes destructives (push, commit)\n5. Max concurrents (1 seule commande à la fois)\n\nUtilisation :\n dev_exec(\"pytest tests/test_atlas.py\")\n dev_exec(\"git add -A && git commit -m 'fix: ...'\")\n dev_exec(\"cd ~/santana && source venv_new/bin/activate && python3 santana.py\")\n\"\"\"\n\nimport os\nimport re\nimport json\nimport logging\nimport subprocess\nimport shutil\nimport time\nimport threading\n\nlogger = logging.getLogger(__name__)\n\n# ─── Configuration ─────────────────────────────────────────────────────────\n\n_DEFAULT_TIMEOUT = 60 # secondes\n_MAX_TIMEOUT = 300 # max absolu (5 min)\n_MAX_OUTPUT = 20000 # caractères max dans la réponse\n\n# Répertoires de travail autorisés\n_WORK_DIRS = [\n os.path.expanduser(\"~/santana\"),\n os.path.expanduser(\"~/.openclaw\"),\n os.path.expanduser(\"~/.hermes\"),\n os.path.expanduser(\"~\"),\n]\n\n# Fichiers critiques : backup automatique avant chaque commande\n_CRITICAL_FILES = [\n \"~/.openclaw/.env\",\n \"~/.openclaw/config.yaml\",\n \"~/.openclaw/openclaw.yaml\",\n \"~/santana/.env\",\n \"~/santana/santana.py\",\n \"~/santana/memory.db\",\n \"~/santana/metrics.db\",\n]\n\n# Patterns à masquer dans les réponses (tokens, secrets, clés)\n_SECRET_PATTERNS = [\n (r'gh[ps]_[a-zA-Z0-9]{36}', 'gh_[xxx]'),\n (r'sk-[a-zA-Z0-9]{32,}', 'sk-[xxx]'),\n (r'Bearer\\s+\\S+', 'Bearer [MASQUÉ]'),\n (r'Authorization:\\s*\\S+', 'Authorization: [MASQUÉ]'),\n (r'x-access-token:[^@]+@', 'x-access-token:[MASQUÉ]@'),\n (r'TELEGRAM_TOKEN=\\S+', 'TELEGRAM_TOKEN=[MASQUÉ]'),\n (r'DEEPSEEK_API_KEY=\\S+', 'DEEPSEEK_API_KEY=[MASQUÉ]'),\n (r'GROQ_API_KEY=\\S+', 'GROQ_API_KEY=[MASQUÉ]'),\n (r'password[=:]\\s*[\"\\\\\\']?\\S+[\"\\\\\\']?', 'password=[MASQUÉ]'),\n]\n\n# Verrou global — 1 commande à la fois\n_exec_lock = threading.Lock()\n\n\n# ─── Helpers ────────────────────────────────────────────────────────────────\n\ndef _expand(path: str) -> str:\n return os.path.expanduser(path)\n\n\ndef _backup_critical():\n \"\"\"Sauvegarde les fichiers critiques avant une commande destructive.\"\"\"\n ts = time.strftime(\"%Y%m%d_%H%M%S\")\n backup_dir = os.path.expanduser(\"~/backups/dev_exec\")\n os.makedirs(backup_dir, exist_ok=True)\n for path in _CRITICAL_FILES:\n src = _expand(path)\n if os.path.exists(src):\n dst = os.path.join(backup_dir, f\"{os.path.basename(src)}.{ts}\")\n try:\n shutil.copy2(src, dst)\n except Exception as e:\n logger.warning(f\"[DEVEXEC] Backup échoué {src}: {e}\")\n\n\ndef _mask_secrets(output: str) -> str:\n \"\"\"Masque les tokens et secrets dans la sortie.\"\"\"\n for pattern, replacement in _SECRET_PATTERNS:\n output = re.sub(pattern, replacement, output)\n return output\n\n\ndef _snapshot_before_destructive(command: str) -> str | None:\n \"\"\"Fait un snapshot git avant les commandes destructives.\"\"\"\n cmd_lower = command.lower()\n is_destructive = (\n \"git push\" in cmd_lower\n or \"git commit\" in cmd_lower\n or \"git merge\" in cmd_lower\n )\n if not is_destructive:\n return None\n\n repos = {\n \"~/santana\": \"Santana\",\n \"~/.openclaw\": \"OpenClaw\",\n \"~/.hermes\": \"Hermes\",\n }\n results = []\n for path, name in repos.items():\n repo_dir = _expand(path)\n git_dir = os.path.join(repo_dir, \".git\")\n if os.path.exists(git_dir):\n try:\n ts = time.strftime(\"%Y-%m-%d %H:%M:%S\")\n subprocess.run(\n [\"git\", \"add\", \"-A\"],\n cwd=repo_dir, capture_output=True, timeout=10\n )\n r = subprocess.run(\n [\"git\", \"stash\", \"push\", \"-m\", f\"[auto-snapshot avant destructive] {ts}\"],\n cwd=repo_dir, capture_output=True, text=True, timeout=10\n )\n results.append(f\"{name}: {r.stdout.strip() or 'stash créé'}\")\n except Exception as e:\n results.append(f\"{name}: snapshot échoué ({e})\")\n\n return \" | \".join(results) if results else None\n\n\n# ─── Outil principal ────────────────────────────────────────────────────────\n\ndef _dev_exec(command: str, timeout: int = _DEFAULT_TIMEOUT) -> str:\n \"\"\"Exécute une commande shell et retourne stdout/stderr.\"\"\"\n global _exec_active\n\n # Rate limiter : 1 commande à la fois\n if not _exec_lock.acquire(blocking=False):\n return json.dumps({\"error\": \"Une commande est déjà en cours d'exécution.\"})\n\n try:\n actual_timeout = min(timeout or _DEFAULT_TIMEOUT, _MAX_TIMEOUT)\n\n # 1. Backup fichiers critiques\n _backup_critical()\n\n # 2. Snapshot git avant actions destructives\n snap_msg = _snapshot_before_destructive(command)\n if snap_msg:\n logger.info(f\"[DEVEXEC] Snapshot: {snap_msg}\")\n\n # 3. Déterminer le répertoire de travail\n workdir = os.path.expanduser(\"~\")\n cd_match = re.search(r'cd\\s+(\\S+)', command)\n if cd_match:\n candidate = os.path.expanduser(cd_match.group(1))\n if os.path.isdir(candidate):\n workdir = candidate\n\n logger.info(f\"[DEVEXEC] Lancement dans {workdir}: {command[:120]}\")\n\n # 4. Exécution\n result = subprocess.run(\n command,\n shell=True,\n capture_output=True,\n text=True,\n timeout=actual_timeout,\n cwd=workdir,\n executable=\"/bin/bash\",\n )\n\n # 5. Collecter et filtrer\n stdout = _mask_secrets(result.stdout or \"\")\n stderr = _mask_secrets(result.stderr or \"\")\n\n output_parts = []\n if stdout:\n output_parts.append(stdout[-_MAX_OUTPUT:])\n if stderr:\n output_parts.append(f\"--- stderr ---\\n{stderr[-5000:]}\")\n if result.returncode != 0:\n output_parts.append(f\"--- code de sortie: {result.returncode} ---\")\n\n final = \"\\n\".join(output_parts) or \"(aucune sortie)\"\n if len(final) > _MAX_OUTPUT:\n final = final[:_MAX_OUTPUT] + \"\\n[... tronqué]\"\n\n logger.info(f\"[DEVEXEC] Terminé: returncode={result.returncode}, {len(final)} chars\")\n return final\n\n except subprocess.TimeoutExpired:\n logger.warning(f\"[DEVEXEC] Timeout après {actual_timeout}s\")\n return json.dumps({\"error\": f\"Timeout {actual_timeout}s — commande tuée.\"})\n except Exception as e:\n logger.error(f\"[DEVEXEC] Erreur: {e}\")\n return json.dumps({\"error\": f\"Erreur: {str(e)}\"})\n finally:\n _exec_lock.release()\n\n\n# ─── Interface outil ────────────────────────────────────────────────────────\n\ndef tool_dev_exec(command: str, timeout: int | None = None) -> str:\n \"\"\"Exécute une commande shell libre sur la VM.\n\n Permet à Santana de modifier, tester et déployer son code ainsi que\n celui des autres agents (OpenClaw, Hermes) sur la machine.\n\n Args:\n command: Commande shell à exécuter (bash, cd, git, python3, pytest, etc.)\n timeout: Timeout en secondes (défaut: 60, max: 300)\n\n Returns:\n stdout + stderr de la commande (secrets masqués)\n \"\"\"\n if not command or not command.strip():\n return json.dumps({\"error\": \"Commande vide.\"})\n return _dev_exec(command, timeout=timeout or _DEFAULT_TIMEOUT)\n"}, "dc5c04c1468be2488d326d89b5d4836f104b3924": {"t": 1785753216.119486, "n": 7616, "h": "20426b6a2408680f", "c": "\"\"\"\ndev_exec.py — Exécution libre de commandes shell pour Santana.\n\nL'outil qui manquait à Santana pour coder librement.\nPas d'allowlist, pas de denylist, pas de sandbox mount.\n\nGarde-fous systémiques uniquement :\n1. Timeout + kill forcé\n2. Filtre post-output (patterns de tokens/secrets masqués)\n3. Backup des fichiers critiques avant exécution\n4. Snapshot git avant commandes destructives (push, commit)\n5. Max concurrents (1 seule commande à la fois)\n\nUtilisation :\n dev_exec(\"pytest tests/test_atlas.py\")\n dev_exec(\"git add -A && git commit -m 'fix: ...'\")\n dev_exec(\"cd ~/santana && source venv_new/bin/activate && python3 santana.py\")\n\"\"\"\n\nimport os\nimport re\nimport json\nimport logging\nimport subprocess\nimport shutil\nimport time\nimport threading\n\nlogger = logging.getLogger(__name__)\n\n# ─── Configuration ─────────────────────────────────────────────────────────\n\n_DEFAULT_TIMEOUT = 60 # secondes\n_MAX_TIMEOUT = 300 # max absolu (5 min)\n_MAX_OUTPUT = 20000 # caractères max dans la réponse\n\n# Répertoires de travail autorisés\n_WORK_DIRS = [\n os.path.expanduser(\"~/santana\"),\n os.path.expanduser(\"~/.openclaw\"),\n os.path.expanduser(\"~/.hermes\"),\n os.path.expanduser(\"~\"),\n]\n\n# Fichiers critiques : backup automatique avant chaque commande\n_CRITICAL_FILES = [\n \"~/.openclaw/.env\",\n \"~/.openclaw/config.yaml\",\n \"~/.openclaw/openclaw.yaml\",\n \"~/santana/.env\",\n \"~/santana/santana.py\",\n \"~/santana/memory.db\",\n \"~/santana/metrics.db\",\n]\n\n# Patterns à masquer dans les réponses (tokens, secrets, clés)\n_SECRET_PATTERNS = [\n (r'gh[ps]_[a-zA-Z0-9]{36}', 'gh_[xxx]'),\n (r'sk-[a-zA-Z0-9]{32,}', 'sk-[xxx]'),\n (r'Bearer\\s+\\S+', 'Bearer [MASQUÉ]'),\n (r'Authorization:\\s*\\S+', 'Authorization: [MASQUÉ]'),\n (r'x-access-token:[^@]+@', 'x-access-token:[MASQUÉ]@'),\n (r'TELEGRAM_TOKEN=\\S+', 'TELEGRAM_TOKEN=[MASQUÉ]'),\n (r'DEEPSEEK_API_KEY=\\S+', 'DEEPSEEK_API_KEY=[MASQUÉ]'),\n (r'GROQ_API_KEY=\\S+', 'GROQ_API_KEY=[MASQUÉ]'),\n (r'password[=:]\\s*[\"\\\\\\']?\\S+[\"\\\\\\']?', 'password=[MASQUÉ]'),\n]\n\n# Verrou global — 1 commande à la fois\n_exec_lock = threading.Lock()\n\n\n# ─── Helpers ────────────────────────────────────────────────────────────────\n\ndef _expand(path: str) -> str:\n return os.path.expanduser(path)\n\n\ndef _backup_critical():\n \"\"\"Sauvegarde les fichiers critiques avant une commande destructive.\"\"\"\n ts = time.strftime(\"%Y%m%d_%H%M%S\")\n backup_dir = os.path.expanduser(\"~/backups/dev_exec\")\n os.makedirs(backup_dir, exist_ok=True)\n for path in _CRITICAL_FILES:\n src = _expand(path)\n if os.path.exists(src):\n dst = os.path.join(backup_dir, f\"{os.path.basename(src)}.{ts}\")\n try:\n shutil.copy2(src, dst)\n except Exception as e:\n logger.warning(f\"[DEVEXEC] Backup échoué {src}: {e}\")\n\n\ndef _mask_secrets(output: str) -> str:\n \"\"\"Masque les tokens et secrets dans la sortie.\"\"\"\n for pattern, replacement in _SECRET_PATTERNS:\n output = re.sub(pattern, replacement, output)\n return output\n\n\ndef _snapshot_before_destructive(command: str) -> str | None:\n \"\"\"Fait un snapshot git avant les commandes destructives.\"\"\"\n cmd_lower = command.lower()\n is_destructive = (\n \"git push\" in cmd_lower\n or \"git commit\" in cmd_lower\n or \"git merge\" in cmd_lower\n )\n if not is_destructive:\n return None\n\n repos = {\n \"~/santana\": \"Santana\",\n \"~/.openclaw\": \"OpenClaw\",\n \"~/.hermes\": \"Hermes\",\n }\n results = []\n for path, name in repos.items():\n repo_dir = _expand(path)\n git_dir = os.path.join(repo_dir, \".git\")\n if os.path.exists(git_dir):\n try:\n ts = time.strftime(\"%Y-%m-%d %H:%M:%S\")\n subprocess.run(\n [\"git\", \"add\", \"-A\"],\n cwd=repo_dir, capture_output=True, timeout=10\n )\n r = subprocess.run(\n [\"git\", \"stash\", \"push\", \"-m\", f\"[auto-snapshot avant destructive] {ts}\"],\n cwd=repo_dir, capture_output=True, text=True, timeout=10\n )\n results.append(f\"{name}: {r.stdout.strip() or 'stash créé'}\")\n except Exception as e:\n results.append(f\"{name}: snapshot échoué ({e})\")\n\n return \" | \".join(results) if results else None\n\n\n# ─── Outil principal ────────────────────────────────────────────────────────\n\ndef _dev_exec(command: str, timeout: int = _DEFAULT_TIMEOUT) -> str:\n \"\"\"Exécute une commande shell et retourne stdout/stderr.\"\"\"\n global _exec_active\n\n # Rate limiter : 1 commande à la fois\n if not _exec_lock.acquire(blocking=False):\n return json.dumps({\"error\": \"Une commande est déjà en cours d'exécution.\"})\n\n try:\n actual_timeout = min(timeout or _DEFAULT_TIMEOUT, _MAX_TIMEOUT)\n\n # 1. Backup fichiers critiques\n _backup_critical()\n\n # 2. Snapshot git avant actions destructives\n snap_msg = _snapshot_before_destructive(command)\n if snap_msg:\n logger.info(f\"[DEVEXEC] Snapshot: {snap_msg}\")\n\n # 3. Déterminer le répertoire de travail\n workdir = os.path.expanduser(\"~\")\n cd_match = re.search(r'cd\\s+(\\S+)', command)\n if cd_match:\n candidate = os.path.expanduser(cd_match.group(1))\n if os.path.isdir(candidate):\n workdir = candidate\n\n logger.info(f\"[DEVEXEC] Lancement dans {workdir}: {command[:120]}\")\n\n # 4. Exécution\n result = subprocess.run(\n command,\n shell=True,\n capture_output=True,\n text=True,\n timeout=actual_timeout,\n cwd=workdir,\n executable=\"/bin/bash\",\n )\n\n # 5. Collecter et filtrer\n stdout = _mask_secrets(result.stdout or \"\")\n stderr = _mask_secrets(result.stderr or \"\")\n\n output_parts = []\n if stdout:\n output_parts.append(stdout[-_MAX_OUTPUT:])\n if stderr:\n output_parts.append(f\"--- stderr ---\\n{stderr[-5000:]}\")\n if result.returncode != 0:\n output_parts.append(f\"--- code de sortie: {result.returncode} ---\")\n\n final = \"\\n\".join(output_parts) or \"(aucune sortie)\"\n if len(final) > _MAX_OUTPUT:\n final = final[:_MAX_OUTPUT] + \"\\n[... tronqué]\"\n\n logger.info(f\"[DEVEXEC] Terminé: returncode={result.returncode}, {len(final)} chars\")\n return final\n\n except subprocess.TimeoutExpired:\n logger.warning(f\"[DEVEXEC] Timeout après {actual_timeout}s\")\n return json.dumps({\"error\": f\"Timeout {actual_timeout}s — commande tuée.\"})\n except Exception as e:\n logger.error(f\"[DEVEXEC] Erreur: {e}\")\n return json.dumps({\"error\": f\"Erreur: {str(e)}\"})\n finally:\n _exec_lock.release()\n\n\n# ─── Interface outil ────────────────────────────────────────────────────────\n\ndef tool_dev_exec(command: str, timeout: int | None = None) -> str:\n \"\"\"Exécute une commande shell libre sur la VM.\n\n Permet à Santana de modifier, tester et déployer son code ainsi que\n celui des autres agents (OpenClaw, Hermes) sur la machine.\n\n Args:\n command: Commande shell à exécuter (bash, cd, git, python3, pytest, etc.)\n timeout: Timeout en secondes (défaut: 60, max: 300)\n\n Returns:\n stdout + stderr de la commande (secrets masqués)\n \"\"\"\n if not command or not command.strip():\n return json.dumps({\"error\": \"Commande vide.\"})\n return _dev_exec(command, timeout=timeout or _DEFAULT_TIMEOUT)\n"}}}}