33800 Docs

← Retour

Audit approfondi - ai-orchestrator

Date : 15-02-2026 (initial) / 15-02-2026 (complete par 2nd pass) Status : IMPLEMENTEE Auditeur : Claude Opus 4.6 Fichiers audites :

Contexte : Service interne, reseau local uniquement. Le nginx en facade n'a PAS private: true dans la conf (voir F27). Pas expose directement sur internet mais accessible via le domaine ai-orchestrator.33800.nowhere84.com.


RESUME EXECUTIF

Le code est globalement solide et bien structure pour un orchestrateur interne. 4675 lignes de Python avec une architecture VRAM-aware bien pensee. Les principaux risques identifies sont :

Au total : 31 findings identifies (20 du premier pass + 11 du second pass).


COUVERTURE DE L'AUDIT

Section (lignes) Contenu Couvert
1-200 Config, imports, constantes, GPU_CONFIG, lifespan OUI
200-450 Models Pydantic, TOOLS_CONFIG (14 tools) OUI
450-750 TOOLS_SCHEMAS, state tracking, VRAM estimation OUI
750-1000 Health checks, SSH, GPU status, stop/start tool OUI
1000-1200 API endpoints (tools, worker control, parallel) OUI
1200-1600 DB functions, queue Redis, webhook callback OUI
1600-2000 Job execution: Bark, MusicGen, ComfyUI, Fooocus OUI
2000-2500 Job execution: Wan2.1 (CLI), FaceFusion (CLI) OUI
2500-2700 Job execution: Ollama (embeddings, chat, generate) OUI
2700-3100 Anthropic (conversion, batch, direct, budget) OUI
3100-3500 Job execution: Applio, TripoSR, YOLO OUI
3500-3900 Job execution: SadTalker, determine_preferred_gpu OUI
3900-4300 Worker principal, process_single_job, watchdog OUI
4300-4675 API endpoints (jobs CRUD, upload, costs, queue_sync) OUI

FINDINGS

F01 - Credentials hardcodes dans le code source

Fichier : main.py lignes 35, 40 Description : Le mot de passe Redis (urefsLoXibTZ36mtuygcLyNuqzOmIVkF) et le mot de passe PostgreSQL (xgvKM65PC3zWspTCXkbxPx2fx5fOHRVK) sont hardcodes en tant que valeurs par defaut des os.environ.get(). La conf de deploiement (conf.prod.gouroubleu.yml) ne les injecte pas via env vars, donc ce sont bien ces valeurs qui sont utilisees en production. Certitude : CERTAIN (verifie en croisant conf.prod.gouroubleu.yml qui ne definit PAS REDISHOST/PASSWORD ni POSTGRES*) Severite : BASSE (service interne, reseau local, git prive) Impact : Si le repo est expose ou clone, les credentials fuient. De plus, toute rotation de mot de passe necessite un rebuild de l'image. Fix propose : Ajouter REDIS_PASSWORD, POSTGRES_PASSWORD, POSTGRES_HOST, POSTGRES_PORT dans la section env: de conf.prod.gouroubleu.yml, ou les injecter via le fichier prod.env de smart-deploy (les variables REDIS_PROD_PASSWORD et SUPABASE_PROD_POSTGRES_PASSWORD existent deja dans /stock_8to/33800-stack/docker/secrets/prod.env).


F02 - WIN11_HOST defini deux fois (redondance qui masque la conf)

Fichier : main.py ligne 185 et conf.prod.gouroubleu.yml ligne 16 Description : WIN11_HOST est defini une premiere fois en haut du fichier comme constante brute WIN11_HOST = "192.168.1.30" (ligne 185), apres avoir potentiellement ete injecte par l'env (conf.prod.gouroubleu.yml ligne 16 definit WIN11_HOST: "192.168.1.30"). Cependant la constante en ligne 185 ecrase toute variable d'environnement que smart-deploy aurait injectee. Les references WIN11_HOST dans TOOLS_CONFIG (lignes 293-422) et les health URLs utilisent cette constante, pas la variable d'env. Certitude : CERTAIN (la ligne 185 est une reassignation pure, pas un os.environ.get) Severite : BASSE (la valeur est identique donc pas de bug actuel, mais si WIN11_HOST devait changer en production, il faudrait modifier le code) Fix propose : Supprimer la ligne 185 et remplacer par WIN11_HOST = os.environ.get("WIN11_HOST", "192.168.1.30"). Attention : les TOOLS_CONFIG sont evalues a l'import donc ils utilisent la valeur au moment du chargement du module -- ce qui est coherent.


F03 - Race condition sur le requeue de jobs quand GPU est unreachable

Fichier : main.py lignes 3948-3955 (dans process_single_job) Description : Quand la pre-verification GPU echoue (vram_free == "N/A"), le job est requeue via queue_push_job apres un db_update_job_status(job_id, "pending"). Mais le job est deja sorti de la queue Redis par le worker (ligne 4248 queue_pop_job). Si le container crashe entre le pop et le requeue, le job est perdu (ni dans la queue ni en cours). Ce n'est pas un bug a proprement parler -- il y a /api/queue/sync pour reconstituer la queue -- mais c'est un risque en cas de crash au mauvais moment. Certitude : THEORIQUE (necessite un crash precis au mauvais moment) Severite : BASSE (le sync endpoint existe pour reparer) Fix propose : Aucun fix urgent. Pour plus de robustesse, on pourrait utiliser un pattern Redis RPOPLPUSH avec une "processing queue" intermediaire, mais c'est de la sur-ingenierie pour un service interne.


F04 - Comment du set_parallel_jobs non coherent avec le code

Fichier : main.py ligne 1134-1139 Description : Le docstring dit "Set number of parallel Ollama jobs (1-5)" mais le max() / min() clamp entre 1 et 10 (count = max(1, min(10, count))). Commentaire inline dit "Clamp between 1 and 5". Ce n'est pas un bug mais une incoherence de documentation. Certitude : CERTAIN Severite : BASSE (documentation) Fix propose : Aligner le commentaire et le docstring avec la borne reelle (1-10), ou changer le clamp a 1-5 si c'est l'intention.


F05 - Cloud tasks (anthropic) dispatched sans slot tracking --> pas de limite de concurrence

Fichier : main.py lignes 4268-4271 (dans job_worker) Description : Quand un job anthropic est dispatche, il cree un asyncio.create_task() mais NE l'ajoute PAS a gpu_slots. Le task n'est suivi nulle part. Consequences :

  1. Le worker peut dispatcher un nombre illimite de jobs anthropic en parallele (un par cycle worker, soit ~1/s).
  2. Si le container s'arrete gracieusement (CancelledError), les tasks anthropic ne sont PAS annulees (le cleanup en lignes 4320-4326 n'itere que gpu_slots).
  3. Le dashboard ne montre pas les jobs anthropic en cours. Certitude : CERTAIN (trace du code : asyncio.create_task(process_single_job(...)) puis continue, aucun ajout a gpu_slots) Severite : MOYENNE Impact : Si 50 jobs anthropic sont en queue, ils seront tous lances quasi simultanement, ce qui peut exploser le budget et generer 50 appels API concurrents. Le budget check est par-job, pas global -- le check_anthropic_budget() lit le cout du mois DEJA ENREGISTRE dans la DB, mais les jobs en vol n'y sont pas encore. Donc 50 jobs peuvent passer le budget check simultanement. Fix propose : Ajouter un compteur cloud_tasks: list[asyncio.Task] et limiter la concurrence (ex: max 3 anthropic en parallele), ou ajouter les cloud tasks dans une structure de suivi separee de gpu_slots.

F06 - Budget Anthropic : race condition entre check et utilisation

Fichier : main.py lignes 2896-2902 (check_anthropic_budget) + 2917 (execute_anthropic_job) Description : Le budget check lit SUM(cost_usd) WHERE status = 'completed' dans la DB. Mais entre le check et l'appel API (qui peut couter plusieurs dollars), d'autres jobs peuvent aussi passer le check. Comme les jobs anthropic sont dispatches sans limite (F05), N jobs peuvent passer le check simultanement si le cout cumule est sous le budget au moment de la lecture. Certitude : PROBABLE (necessite des jobs concurrents, ce qui est possible vu F05) Severite : MOYENNE (peut depasser le budget de $ANTHROPIC_BUDGET_USD x N_jobs_concurrents) Fix propose : Utiliser un lock Redis ou un compteur atomique pour reserver le budget avant l'appel API, ou limiter la concurrence des jobs anthropic.


F07 - Watchdog GPU : crash possible sur parsing de VRAM quand GPU unreachable

Fichier : main.py lignes 4205-4208 (watchdog dans job_worker) Description : Le watchdog fait vram_used = int(gpu.vram_used.replace(" Mo", "")) et gpu_util = int(gpu.gpu_util.replace("%", "")). Si la GPU est unreachable, get_gpu_status() retourne un fallback avec vram_used="N/A". Le int("N/A".replace(" Mo", "")) leve un ValueError. Cependant, ce code est dans un try/except Exception: pass (ligne 4221-4222), donc le crash est silencieux. Le job ne sera jamais tue par le watchdog si le GPU est unreachable -- ce qui est probablement le bon comportement. Certitude : CERTAIN (le crash arrive mais est catch) Severite : BASSE (le catch est correct, mais le code est fragile) Fix propose : Ajouter un guard if gpu.vram_used == "N/A": continue avant le parsing, pour la lisibilite.


F08 - active_tools tracking incoherent avec gpu_slots

Fichier : main.py lignes 4224-4232 + ligne 735 Description : Le dictionnaire active_tools est un Dict[int, Optional[str]] (un seul tool par GPU), mais gpu_slots permet plusieurs tools par GPU (multiple server tools concurrents). Le worker met a jour active_tools a chaque cycle pour afficher le tool "le plus significatif". Cependant, d'autres parties du code utilisent active_tools pour des decisions :

Apres analyse approfondie, ce n'est pas un bug dans le flux actuel car le worker orchestre tout via can_dispatch(), mais c'est un design qui pourrait causer des confusions si on ajoute de la logique basee sur active_tools sans passer par le worker. Certitude : THEORIQUE Severite : BASSE Fix propose : Documenter que active_tools est un raccourci pour le dashboard et ne doit pas etre utilise pour des decisions de scheduling. Ou le renommer en dashboard_active_tools.


F09 - Ollama server VRAM estimation ne prend pas en compte les modeles deja charges

Fichier : main.py lignes 4171-4176 (dans can_dispatch) Description : Pour les server tools (Ollama), le VRAM est estime comme max(vram des slots en cours). Cela suppose que tous les jobs Ollama utilisent le meme modele (un seul modele charge). Si deux jobs demandent des modeles differents (ex: qwen3:8b = 5000 MB et devstral = 20000 MB), Ollama charge le nouveau modele et le VRAM reel est la somme (si keep_alive > 0), pas le max.

Le vrai probleme serait : 2 jobs concurrents demandent 2 modeles differents sur le meme GPU. Le premier charge modele A (5 GB), le second demande modele B (5 GB). Ollama va charger B tout en gardant A (keep_alive=30m sur GPU0). Vrai VRAM = 10 GB. Estimation = max(5, 5) = 5 GB. Le can_dispatch autorisera un 3eme job qui pourrait causer un OOM. Certitude : PROBABLE (depend de la combinaison de modeles et du timing) Severite : MOYENNE (OOM = crash Ollama = job fail) Fix propose : Pour les server tools, tracker le modele dans le slot et utiliser sum si les modeles sont differents, ou max si identiques. Alternativement, querier Ollama /api/ps pour connaitre les modeles reellement charges.


F10 - Anthropic batch polling dans un seul client httpx (connexion timeout apres 2h)

Fichier : main.py lignes 2994-3065 (execute_anthropic_batch_job) Description : Le client httpx est cree avec async with httpx.AsyncClient(timeout=httpx.Timeout(30.0, connect=10.0)) as client: et toute la boucle de polling (potentiellement 2h = 240 polls * 30s) se fait dans ce meme async with. Les connexions HTTP/2 et keepalive peuvent expire bien avant 2h, causant des erreurs de connexion. Chaque poll_resp = await client.get(...) pourrait echouer si la connexion sous-jacente est fermee.

httpx reessaie les connexions dans un pool, donc ca ne crash probablement pas, mais c'est un risque. Certitude : THEORIQUE Severite : BASSE Fix propose : Creer un nouveau httpx.AsyncClient pour chaque poll, ou deplacer le async with autour de chaque requete individuelle.


F11 - SSH command injection possible via job parameters

Fichier : main.py lignes 810-826 (run_ssh_command) + multiples appelants Description : run_ssh_command echappe les double-quotes (command.replace('"', '\\"')) mais pas les backticks, dollar, backslash, etc. Si un parametre utilisateur est interpole dans une commande SSH, il peut injecter des commandes.

Trace des paths :

Pour facefusion : le job est cree via POST /api/jobs qui ne valide pas les valeurs des input_params au-dela du type Dict. Certitude : CERTAIN pour le path facefusion (le code interpole sans sanitisation) Severite : MOYENNE (service interne, pas expose sur internet, mais un utilisateur malveillant sur le reseau local pourrait executer des commandes arbitraires sur win11) Fix propose : Sanitiser face_swapper_model et face_enhancer_model avec une whitelist de caracteres alphanumeriques + underscore + point. Ou mieux : utiliser une liste predeterminee de modeles valides. Le pattern base64 utilise pour wan21 est le bon pattern a generaliser.


F12 - Fichier upload : ecrasement silencieux de fichiers existants

Fichier : main.py lignes 4511-4537 (upload_input_file) Description : Un upload de fichier ecrase silencieusement un fichier existant avec le meme nom. file_path = f"{INPUTS_PATH}/{filename}" + with open(file_path, "wb"). Si deux jobs different referent le meme nom de fichier d'entree, l'un peut ecraser l'autre. Certitude : CERTAIN Severite : BASSE (pour un service interne, c'est souvent le comportement voulu) Fix propose : Ajouter un prefixe unique (timestamp ou UUID) au nom de fichier, ou verifier l'existence et retourner un conflit 409.


F13 - Cleanup SadTalker : asyncio.create_task sur un subprocess non-awaited

Fichier : main.py ligne 3824 Description : asyncio.create_task(asyncio.create_subprocess_shell(cleanup_cmd)) cree une task qui cree un subprocess, mais le subprocess retourne un Process object qu'il faut await .wait() pour eviter un zombie process. La task attend le create_subprocess_shell (qui retourne rapidement le Process) mais personne n'attend .wait() ou .communicate(). Certitude : CERTAIN (le subprocess est lance mais jamais reaped) Severite : BASSE (c'est juste un cleanup, et le process termine de lui-meme -- le zombie sera reap au prochain GC ou a la fin du event loop) Fix propose : Wrapper dans une async function : async def _cleanup(): proc = await asyncio.create_subprocess_shell(cmd); await proc.wait() puis asyncio.create_task(_cleanup()).


F14 - generate job_type : Ollama /api/generate ne recoit pas num_ctx

Fichier : main.py lignes 2654-2663 (execute_ollama_job, mode "generate") Description : Pour le job_type chat, num_ctx est passe dans options (ligne 2577-2588). Mais pour le job_type generate (default), num_ctx n'est PAS passe. Cela signifie qu'Ollama utilise son default (qui peut etre 128k+ pour certains modeles comme Qwen3), causant une allocation VRAM excessive.

Le MEMORY.md utilisateur dit explicitement : "num_ctx OBLIGATOIRE sinon Ollama alloue pour le full context window (128k+) -> OOM". Certitude : CERTAIN (la MEMORY de l'utilisateur confirme le probleme, et le code le confirme : pas de num_ctx dans le mode generate) Severite : HAUTE (OOM = crash Ollama = tous les jobs LLM echouent) Fix propose : Ajouter "num_ctx": params.get("num_ctx", 8192) dans les options du mode generate (ligne ~2660).


F15 - cost_summary endpoint : annee/mois hardcodes en default

Fichier : main.py ligne 4595 Description : async def costs_summary(period: str = "month", year: int = 2026, month: int = 2) hardcode year=2026, month=2 comme defaults. A partir de mars 2026, le endpoint retournera par defaut les couts de fevrier sauf si le client passe explicitement year/month. Certitude : CERTAIN Severite : BASSE (l'API est correcte si on passe les bons params, c'est juste un default qui va devenir obsolete) Fix propose : Utiliser datetime.now().year et datetime.now().month comme defaults.


F16 - CORS wildcard avec allow_credentials=True

Fichier : main.py lignes 176-182 Description : allow_origins=["*"] avec allow_credentials=True est techniquement invalide selon la spec CORS (les navigateurs ignoreront le * quand credentials sont true). En pratique, ca marche souvent grace aux implementations permissives, mais c'est un anti-pattern. Certitude : CERTAIN (code present) Severite : BASSE (service interne, et en pratique les appels viennent de connectors-api pas de navigateurs) Fix propose : Soit retirer allow_credentials=True, soit lister les origines specifiques (ex: ["https://dashboard.nowhere84.com", "https://connectors.33800.nowhere84.com"]).


F17 - Pas de init.py dans le dossier app/

Fichier : /stock_8to/33800-stack/projects/ai-orchestrator/app/ (manquant) Description : Le dossier app/ ne contient pas de __init__.py. Uvicorn importe app.main:app qui fonctionne sans __init__.py en Python 3.3+ (namespace packages), mais c'est une bonne pratique de l'ajouter pour eviter des surprises avec certains outils (pytest, mypy, etc.). Certitude : CERTAIN (verifie par glob) Severite : BASSE Fix propose : Ajouter un fichier app/__init__.py vide.


F18 - Bare except clauses (anti-pattern Python)

Fichier : main.py lignes 783, 841, 1503, 2076, 2242, 2256, 3263, 3857, 4037, 4298, 4374, et autres Description : Multiples except: sans type d'exception specifie (au moins 11 occurrences). Cela catch aussi KeyboardInterrupt, SystemExit, et GeneratorExit, ce qui peut empecher un arret propre du service ou masquer des bugs critiques. Certitude : CERTAIN (present dans le code) Severite : BASSE (en pratique, uvicorn gere le shutdown via CancelledError et signals, pas via ces exceptions) Fix propose : Remplacer except: par except Exception: partout.


F19 - SSH StrictHostKeyChecking=no partout

Fichier : main.py ligne 814, 1024, et usages directs Description : Toutes les connexions SSH desactivent la verification de la cle host (-o StrictHostKeyChecking=no). Cela rend le service vulnerable a une attaque MITM (un attaquant sur le reseau local pourrait intercepter la communication avec win11). Certitude : CERTAIN Severite : BASSE (reseau local prive, le risque MITM est minimal) Fix propose : Generer une cle host dans un fichier known_hosts monte en volume, et utiliser -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/path/known_hosts.


F20 - start_tool escape des backslashes pour Windows peut casser les chemins

Fichier : main.py ligne 1023 Description : escaped_cmd = final_cmd.replace('\\', '\\\\') double tous les backslashes. final_cmd contient des chemins Windows comme C:\Users\gouro\start_ollama_gpu0.bat qui en Python brut sont C:\\Users\\gouro\\start_ollama_gpu0.bat (grace au r"" raw string dans TOOLS_CONFIG). Apres le replace, ca devient C:\\\\Users\\\\gouro\\\\start_ollama_gpu0.bat. Ensuite c'est passe dans ssh ... "cmd /c {escaped_cmd}". La chaine SSH transmet les doubles backslashes a cmd /c sur Windows, ou \\\\ est interprete comme \\ (UNC path), pas comme \ (path separator local).

Cependant, si ca fonctionnait en production, c'est que SSH+cmd.exe fait une double desescalade. Le " wrapping de la commande SSH cause un premier niveau de desescaping, puis cmd.exe en fait un second. C'est un castle de cards qui fonctionne par accident. Certitude : THEORIQUE (ca fonctionne visiblement en production, sinon aucun tool ne demarrerait) Severite : BASSE (fonctionne actuellement) Fix propose : Aucun fix urgent (ne pas toucher a ce qui marche). Mais si des chemins avec des espaces ou caracteres speciaux sont ajoutes, ca cassera. Un refactoring vers une approche SSH+PowerShell serait plus robuste.


F21 - Path traversal dans execute_facefusion_job via absolute paths (NOUVEAU)

Fichier : main.py lignes 2317-2318 Description : Les parametres source_file et target_file de FaceFusion acceptent des chemins absolus sans restriction :

source_path = f"{INPUTS_PATH}/{source_file}" if not source_file.startswith("/") else source_file
target_path = f"{INPUTS_PATH}/{target_file}" if not target_file.startswith("/") else target_file

Si source_file commence par /, le chemin absolu est utilise tel quel. Un utilisateur peut donc lire n'importe quel fichier du systeme de fichiers en le passant comme source (ex: source_file: "/etc/passwd"). Le fichier est ensuite transfere sur win11 via SCP.

Le meme pattern existe dans :

Les endpoints upload/download (lignes 4447-4589) verifient correctement .. et /, mais les fonctions d'execution ne verifient PAS les chemins absolus dans les input_params. Certitude : CERTAIN (le code startswith("/") bypasse intentionnellement le prefixe INPUTS_PATH) Severite : MOYENNE (service interne, mais permet la lecture de fichiers arbitraires sur le container Docker) Impact : Un attaquant sur le reseau local peut lire des fichiers depuis le container en les envoyant comme input a un job YOLO/FaceFusion/etc. Les volumes montes (/mnt/stock_8to/33800-stack/ai-data et /home/appuser/.ssh) sont accessibles. Fix propose : Supprimer le bypass startswith("/") et forcer tous les chemins a etre relatifs a INPUTS_PATH. Si des chemins absolus sont necessaires, verifier qu'ils sont dans un repertoire autorise avec os.path.realpath() + startswith(INPUTS_PATH).


F22 - Imports repetes dans les fonctions (performance mineure) (NOUVEAU)

Fichier : main.py lignes 1496, 1681, 1734, 1794, 1973, 2049, 2073, 2117, 2514, 2914, 2969, 3085, 3255, 3319, 3516, 3621, 3731, 3578, 1804, 1984 Description : import aiohttp est repete dans 11 fonctions differentes. import base64, import random, et import json sont aussi repetes dans des fonctions locales. En Python, les imports dans les fonctions sont caches dans sys.modules et sont rapides apres le premier appel, mais c'est une pratique inhabituelle qui rend le code plus difficile a lire et a maintenir. Le import json a la ligne 3255 est particulierement inutile puisque json est deja importe globalement en ligne 16. Certitude : CERTAIN Severite : BASSE (style/maintenabilite, pas de bug) Fix propose : Deplacer import aiohttp, import base64, import random en imports globaux en haut du fichier. Supprimer les imports locaux redondants de json.


F23 - Anthropic tool_id = "anthropic" dans active_tools avec gpu_id = -1 (NOUVEAU)

Fichier : main.py lignes 425-434 (TOOLS_CONFIG["anthropic"]) + ligne 4003-4004 Description : L'outil anthropic est configure avec gpu_id=-1 (ligne 430) et port=0. Dans process_single_job, le code verifie tool.gpu_id is not None and tool.gpu_id >= 0 (ligne 3947, 4003, 4117) pour eviter de toucher aux outils cloud. C'est correct. Mais la variable is_cloud est redefinie une deuxieme fois a la ligne 4045 (is_cloud = tool_id in CLOUD_TOOLS) alors qu'elle a deja ete definie a la ligne 3943. Cette double definition est inoffensive (meme valeur) mais trompeuse. Si quelqu'un modifiait tool_id entre les deux (ce qui n'arrive pas actuellement), le resultat serait incoherent. Certitude : CERTAIN (double definition) Severite : BASSE (pas de bug actuel, juste du code redondant) Fix propose : Supprimer la ligne 4045 (is_cloud = tool_id in CLOUD_TOOLS) car la variable est deja definie.


F24 - check_tool_health utilise curl en subprocess au lieu de httpx/aiohttp (NOUVEAU)

Fichier : main.py lignes 829-842 Description : check_tool_health shell-out vers curl via asyncio.create_subprocess_shell pour chaque health check. Cela cree un processus OS par check (fork + exec). Les health checks sont appeles :

Chaque check cree aussi un shell inutile (via create_subprocess_shell au lieu de create_subprocess_exec).

De plus, la health_url est passee directement a la commande shell sans echappement. Bien que les URLs viennent de TOOLS_CONFIG (hardcode, pas de l'utilisateur), c'est un vecteur potentiel si on ajoutait des URLs dynamiques. Certitude : CERTAIN Severite : BASSE (performance + style, pas de bug fonctionnel) Fix propose : Remplacer par un appel aiohttp ou httpx direct (deja present dans les imports via d'autres fonctions). Cela eviterait de forker des processus et serait plus rapide.


F25 - set_parallel_jobs utilise SSH directement sans run_ssh_command (NOUVEAU)

Fichier : main.py lignes 1148-1155 Description : Le endpoint set_parallel_jobs execute ssh gouro@{WIN11_HOST} "setx OLLAMA_NUM_PARALLEL {count}" en construisant la commande SSH manuellement, sans passer par run_ssh_command(). Cela signifie :

  1. Pas de -o StrictHostKeyChecking=no (donc depend du known_hosts local)
  2. Pas de -o ConnectTimeout=10
  3. Pas d'echappement des quotes via la logique existante
  4. Le timeout est gere par proc.wait() sans asyncio.wait_for, donc si SSH hang, la requete hang indefiniment (le worker FastAPI est bloque jusqu'a ce que le client abandonne)

Le parametre count est un int donc pas d'injection possible. Certitude : CERTAIN (le code est different de run_ssh_command) Severite : BASSE (le count est un int donc safe, mais le hang potentiel est un souci pour l'API) Fix propose : Utiliser run_ssh_command(f'setx OLLAMA_NUM_PARALLEL {count}') pour beneficier des timeouts et de la gestion d'erreur existante.


F26 - Fooocus seed utilise 263 au lieu de 232 (NOUVEAU)

Fichier : main.py ligne 1985 Description : seed = random.randint(0, 2**63 - 1) pour Fooocus, alors que ComfyUI utilise random.randint(0, 2**32 - 1) (ligne 1805). Fooocus utilise aussi "image_seed": seed dans l'API. Si Fooocus traite le seed comme un int32 en interne (comme la plupart des generateurs de bruit), les valeurs > 2^31 pourraient causer un overflow silencieux ou un comportement inattendu. Certitude : THEORIQUE (depend de l'implementation interne de Fooocus) Severite : BASSE (ne cause pas de crash, juste potentiellement un seed different de l'attendu) Fix propose : Aligner sur 2**32 - 1 pour etre coherent avec ComfyUI et les conventions standard.


F27 - Nginx conf sans private: true --> API exposee publiquement (NOUVEAU)

Fichier : conf.prod.gouroubleu.yml lignes 55-58 (section nginx) Description : La configuration nginx ne contient PAS private: true. Le domaine ai-orchestrator.33800.nowhere84.com est resolu publiquement (DNS pointe vers 82.65.119.221 = IP publique Freebox). Sans private: true, smart-deploy ne genere pas de restriction IP dans la config nginx. Cela signifie que l'API est potentiellement accessible depuis internet.

Le premier audit disait "nginx private:true. Pas expose sur internet." -- c'est FAUX d'apres la conf reelle.

Impact : Toute personne connaissant le domaine peut :

Cependant, la Freebox doit avoir du port forwarding configure pour que le trafic arrive au container. Si le port 443 est forwarde vers nginx (192.168.1.104), alors oui, c'est accessible. Certitude : CERTAIN (la conf n'a pas private: true) -- l'exposition reelle depend du port forwarding Freebox Severite : MOYENNE (si accessible publiquement, c'est un risque budgetaire et d'integrite) Fix propose : Ajouter private: true dans la section nginx: de conf.prod.gouroubleu.yml. Cela generera des restrictions par IP dans la conf nginx, limitant l'acces au reseau local et aux IPs autorisees.


F28 - SadTalker job_id interpole dans des chemins SSH sans sanitisation (NOUVEAU)

Fichier : main.py lignes 3688, 3694, 3704, 3714-3718, 3748, 3754, 3780, 3803, 3823 Description : Le job_id est un UUID genere par uuid.uuid4() (ligne 1319), donc il ne contient que [a-f0-9-]. C'est safe. Cependant, si cette hypothese changeait (ex: job IDs bases sur un input utilisateur), les multiples interpolations dans les commandes SSH de SadTalker deviendraient dangereuses :

mkdir_cmd = f'... I:\\SadTalker\\results\\{job_id}"'
image_remote = f"/I:/SadTalker/inputs/{job_id}_{image_filename}"

Ce n'est PAS un bug actuel mais un couplage implicite entre db_create_job (qui genere le UUID) et toutes les fonctions d'execution (qui interpolent sans verifier). Certitude : THEORIQUE (le UUID est safe actuellement) Severite : BASSE (defense in depth) Fix propose : Rien d'urgent. Si le format de job_id change, il faudra ajouter une validation regex dans les fonctions d'execution.


F29 - Applio hardcode 60 parametres fragile (NOUVEAU)

Fichier : main.py lignes 3180-3226 Description : L'API Applio est appelee via enforce_terms avec un tableau de 60 parametres positionels. Les parametres sont mappes par index (pas par nom) et documentes uniquement par des commentaires inline. Si Applio ajoute ou supprime un parametre dans une mise a jour, l'index de tous les parametres suivants se decale et les appels echouent silencieusement (pas d'erreur, mais des parametres mal places).

De plus, le parametre 7 (f"I:\\Applio\\assets\\audios\\output_{job_id}") est un chemin Windows hardcode. Si Applio est reinstalle dans un autre dossier, ce chemin cassera. Certitude : CERTAIN (le code est fragile par nature) Severite : BASSE (fonctionne pour la version actuelle, mais maintenance risquee) Fix propose : Documenter la version exacte d'Applio contre laquelle ces 60 params sont valides. Ajouter un test d'integration qui verifie la compatibilite.


F30 - Wan2.1 wrapper script pas nettoye en cas de succes (NOUVEAU)

Fichier : main.py lignes 2180-2293 Description : Le job Wan2.1 cree un wrapper Python script sur win11 (outputs/run_{job_id}.py, ligne 2215-2217). En cas de succes, ce script n'est jamais supprime. Seul le fichier video est copie en retour. Les scripts s'accumulent dans I:\Wan2.1\outputs\ sur win11.

En comparaison, SadTalker a un cleanup (ligne 3823-3824, meme s'il est bugge - voir F13) et FaceFusion a un cleanup (rmdir /s /q, ligne 2476-2478). Certitude : CERTAIN (pas de cleanup dans execute_wan21_job pour le wrapper script) Severite : BASSE (disk space sur win11, pas de consequence fonctionnelle) Fix propose : Ajouter un cleanup apres le SFTP get : await run_ssh_command(f'del I:\\Wan2.1\\outputs\\run_{job_id}.py 2>nul').


F31 - db_update_job_status SQL dynamique sans validation des colonnes (NOUVEAU)

Fichier : main.py lignes 1401-1478 Description : La fonction db_update_job_status accepte des **kwargs et construit une requete SQL dynamique en concatenant les noms de colonnes. Les noms de colonnes sont hardcodes dans le if/elif chain (output_result, error_message, processing_time_ms, etc.), donc il n'y a PAS d'injection SQL possible via les noms de colonnes. Les valeurs passent par des parametres ($1, $2, etc.) qui sont safe.

Cependant, les kwargs non reconnus sont silencieusement ignores. Si un appelant passe db_update_job_status(job_id, "completed", typo_field=42), aucune erreur n'est levee et le champ n'est pas sauvegarde. Cela pourrait masquer des bugs dans les appelants. Certitude : CERTAIN (les kwargs inconnus sont ignores) Severite : BASSE (pas de risque de securite, juste un risque de perte de donnees silencieuse) Fix propose : Ajouter un logger.warning pour les kwargs non reconnus, ou lever une ValueError.


RESUME PAR SEVERITE

HAUTE (1)

# Finding Certitude
F14 Ollama generate sans num_ctx -> OOM possible CERTAIN

MOYENNE (5)

# Finding Certitude
F05 Jobs anthropic sans limite de concurrence CERTAIN
F06 Race condition budget anthropic PROBABLE
F09 VRAM estimation Ollama multi-modeles sous-estimee PROBABLE
F11 SSH injection via facefusion params CERTAIN
F21 Path traversal via chemins absolus dans jobs (facefusion, applio, yolo, sadtalker, triposr) CERTAIN
F27 Nginx conf sans private:true -> API potentiellement exposee publiquement CERTAIN

BASSE (25)

# Finding Certitude
F01 Credentials hardcodes CERTAIN
F02 WIN11_HOST defini deux fois CERTAIN
F03 Race condition requeue au crash THEORIQUE
F04 Commentaire parallel jobs incoherent CERTAIN
F07 Watchdog crash silencieux sur N/A CERTAIN
F08 active_tools vs gpu_slots incoherence THEORIQUE
F10 Batch polling dans un seul client httpx THEORIQUE
F12 Upload ecrasement silencieux CERTAIN
F13 SadTalker cleanup zombie process CERTAIN
F15 Defaults annee/mois hardcodes CERTAIN
F16 CORS wildcard + credentials CERTAIN
F17 Pas de init.py CERTAIN
F18 Bare except clauses (11+ occurrences) CERTAIN
F19 StrictHostKeyChecking=no CERTAIN
F20 Escape backslashes fragile THEORIQUE
F22 Imports repetes dans les fonctions CERTAIN
F23 Variable is_cloud redefinie inutilement CERTAIN
F24 Health check via curl subprocess CERTAIN
F25 set_parallel_jobs SSH sans timeout CERTAIN
F26 Fooocus seed 2^63 vs 2^32 THEORIQUE
F28 job_id interpole sans validation explicite THEORIQUE
F29 Applio 60 parametres positionels fragiles CERTAIN
F30 Wan2.1 wrapper script jamais nettoye CERTAIN
F31 db_update_job_status kwargs ignores silencieusement CERTAIN

ANALYSE DETAILLEE PAR SECTION

Section 1-200 : Config, imports, constantes, lifespan

Points audites :

Rien de critique manque dans cette section.

Section 200-450 : Models Pydantic, TOOLS_CONFIG

Points audites :

Section 450-750 : TOOLS_SCHEMAS, VRAM estimation

Points audites :

Section 750-1000 : Health checks, SSH, stop/start tool

Points audites :

Section 1000-1200 : API endpoints worker/tools

Points audites :

Section 1200-1600 : DB functions, queue Redis

Points audites :

Section 1600-2000 : Job execution Bark, MusicGen, ComfyUI, Fooocus

Points audites :

Section 2000-2500 : Wan2.1 CLI, FaceFusion CLI

Points audites :

Section 2500-2700 : Ollama jobs

Points audites :

Section 2700-3100 : Anthropic conversion, execution

Points audites :

Section 3100-3500 : Applio, TripoSR, YOLO

Points audites :

Section 3500-3900 : SadTalker, determine_preferred_gpu

Points audites :

Section 3900-4300 : Worker, process_single_job

Points audites :

Section 4300-4675 : API endpoints CRUD, upload, costs

Points audites :


POINTS POSITIFS

  1. Architecture GPU dual bien pensee : Le routing VRAM-aware avec affinite modele (determine_preferred_gpu) est bien concu et couvre les cas principaux.
  2. Stale job cleanup au demarrage : Le code en lignes 121-132 nettoie correctement les jobs orphelins apres un restart.
  3. Worker pause/resume propre : Le pattern avec le hook pre_deploy qui pause, et le startup qui resume, est robuste.
  4. Watchdog GPU actif : La detection des jobs morts (idle_strikes) est un bon mecanisme de recovery.
  5. Queue sync endpoint : Le /api/queue/sync permet de recuperer d'un desync Redis/PostgreSQL.
  6. Path traversal prevention sur upload/download : Les endpoints upload/download verifient .. et / dans les noms de fichiers.
  7. Cost tracking Anthropic : Le suivi des couts par job avec cache_read/cache_creation est complet et precis.
  8. Webhook callbacks : Le pattern callback_url est propre et non-bloquant (erreurs loguees, pas propagees).
  9. Conversion Ollama<->Anthropic : Les fonctions de conversion sont bien ecrites et gerent les tool_calls correctement.
  10. GPU status cache : Le cache 30s evite de spammer SSH pour nvidia-smi.
  11. Prompt base64 encoding : Le pattern Wan2.1 d'encoder le prompt en base64 avant de le passer a SSH est la bonne approche pour eviter les problemes d'echappement.
  12. Dual-GPU routing intelligent : La logique GPU0=heavy, GPU1=lightweight est bien implementee avec fallback automatique.
  13. Timeout par outil : Les timeouts sont adaptes a chaque type d'outil (1h pour video, 5min pour LLM).
  14. Graceful shutdown : Le handler CancelledError dans le worker annule proprement les tasks GPU en cours.
  15. JSON serialization robuste : json.dumps(value, default=str) evite les crashes sur les types non-serialisables (datetime, UUID, etc.).

ACTIONS RECOMMANDEES (par priorite)

  1. F14 : Ajouter num_ctx: 8192 dans le mode generate d'Ollama (5 min, evite OOM) -- URGENCE: peut causer un OOM a tout moment
  2. F27 : Ajouter private: true dans nginx conf (2 min, securise l'API) -- URGENT si le port forwarding est actif
  3. F21 : Supprimer le bypass startswith("/") dans les fonctions d'execution (15 min, corrige le path traversal dans 5 fonctions)
  4. F05 : Limiter la concurrence des jobs anthropic (30 min)
  5. F11 : Sanitiser les params facefusion (15 min, corrige l'injection SSH)
  6. F09 : Ameliorer l'estimation VRAM multi-modeles ou querier /api/ps (1h)
  7. F01 : Externaliser les credentials dans les env vars (15 min)
  8. F06 : Ajouter un lock budget atomique (30 min)
  9. Le reste (F02-F04, F07-F08, F10, F12-F13, F15-F20, F22-F26, F28-F31) est du cleanup optionnel qui peut etre fait au fil de l'eau.

STATISTIQUES DE L'AUDIT

Metrique Valeur
Lignes de code auditees 4675
Findings totaux 31
Severite HAUTE 1
Severite MOYENNE 6
Severite BASSE 24
Certitude CERTAIN 23
Certitude PROBABLE 2
Certitude THEORIQUE 6
Points positifs identifies 15
Actions recommandees prioritaires 8