Date : 15-02-2026 (initial) / 15-02-2026 (complete par 2nd pass) Status : IMPLEMENTEE Auditeur : Claude Opus 4.6 Fichiers audites :
/stock_8to/33800-stack/projects/ai-orchestrator/app/main.py (4675 lignes, lu en entier x2)/stock_8to/33800-stack/projects/ai-orchestrator/conf.prod.gouroubleu.yml/stock_8to/33800-stack/projects/ai-orchestrator/requirements.txt/stock_8to/33800-stack/projects/ai-orchestrator/Dockerfile/stock_8to/33800-stack/projects/ai-orchestrator/docker-compose.yml/stock_8to/33800-stack/docker/secrets/prod.env (pour cross-ref env vars)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.
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).
| 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 |
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).
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.
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.
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.
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 :
gpu_slots).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.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.
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.
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 :
start_tool() (ligne 1002-1009) verifie active_tools.get(gpu_id) pour savoir si un tool est deja actif, et refuse de demarrer si current_on_gpu != tool_id sans force=true.process_single_job() (ligne 3976-3981) verifie aussi active_tools pour decider de requeue.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.
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.
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.
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 :
execute_wan21_job : le prompt est encode en base64 avant d'etre passe a SSH (ligne 2182-2194). Safe.execute_sadtalker_job : les noms de fichiers (source_image, driven_audio) sont interpoles dans des chemins SFTP (lignes 3694-3710) et dans un script Python (lignes 3733-3744). Les noms de fichiers passent par os.path.basename() (lignes 3693, 3703). Partiellement safe (un nom de fichier avec des caracteres speciaux pourrait casser le script Python mais pas injecter de commandes SSH).execute_facefusion_job : face_swapper_model (ligne 2394) et face_enhancer_model (ligne 2399) sont interpoles directement dans cli_cmd qui est passe a SSH. Si un utilisateur envoie face_swapper_model: "inswapper_128 && rm -rf /", la commande SSH l'executera.stop_tool : le task_name (ligne 964) est derive de tool_id qui vient de la config, pas de l'utilisateur. Safe.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.
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.
asyncio.create_task sur un subprocess non-awaitedFichier : 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()).
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).
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.
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"]).
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.
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.
StrictHostKeyChecking=no partoutFichier : 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.
start_tool escape des backslashes pour Windows peut casser les cheminsFichier : 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.
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 :
execute_applio_job ligne 3132 (audio_file)execute_triposr_job ligne 3331 (image_file)execute_yolo_job ligne 3530 (image_file)execute_sadtalker_job lignes 3675-3676 (source_image, driven_audio)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).
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.
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.
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 :
refresh_tools_status : 14 tools en parallele = 14 processusprocess_single_job : avant chaque job GPUworker_pause : pour chaque toolstart_tool : dans la boucle de polling (toutes les 5s pendant startup_time)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.
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 :
-o StrictHostKeyChecking=no (donc depend du known_hosts local)-o ConnectTimeout=10proc.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.
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.
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 :
GET /api/gpu)GET /api/tools, /api/tools/schemas)POST /api/jobs) -- y compris des jobs Anthropic qui coutent de l'argentPOST /api/upload)GET /api/jobs/{id}, /api/jobs/{id}/output/{file})GET /api/costs/live)POST /api/worker/pause|resume)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.
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.
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.
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').
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.
| # | Finding | Certitude |
|---|---|---|
| F14 | Ollama generate sans num_ctx -> OOM possible | CERTAIN |
| # | 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 |
| # | 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 |
Points audites :
redis.asyncio et asyncpg sont les bons choix pour async.ai: coherent. DEFAULT_PARALLEL_JOBS=2 est raisonnable.generating et loading_model en failed. Correct. Ne requeue PAS les jobs pending (ils sont deja dans Redis). Le risque est que Redis soit vide apres un restart (les queues sont persistees si Redis a de l'AOF/RDB actif).max_server_concurrent 8 pour GPU0, 2 pour GPU1.Rien de critique manque dans cette section.
Points audites :
priority avec ge=-1, le=1 est correct. callback_url optionnel. Job model a tous les champs necessaires.stop_cmd utilisent netstat + taskkill par port, ce qui est robuste. Les start_cmd utilisent des raw strings (r"...") pour les backslashes Windows. Les ports sont uniques par outil. Les startup_time sont raisonnables (30-180s). Le tool anthropic a gpu_id=-1 et port=0 ce qui est correct pour un outil cloud.whisper n'est PAS dans TOOLS_CONFIG malgre la reference dans les MCP tools (ai_job). C'est un outil manquant ou qui n'a jamais ete implemente.Points audites :
default des modeles sont "mistral:latest" mais en pratique qwen3:8b est utilise (d'apres MEMORY.md). C'est un default de documentation, pas un bug car le modele est passe dans les params du job._estimate_ollama_vram (741-764) : Estimation basee sur le nom du modele (regex sur "70b", "32b", etc.). Default a 4500 MB (~7B). La regex est fragile : "1b" in m matcherait aussi "21b" (faux) ou "1billion" (faux). Mais en pratique, les noms de modeles Ollama sont standardises ("qwen3:8b", "llama3.2:1b", etc.). Le "0.5b" est a la meme priorite que "1b", ce qui est correct (1000 MB).estimate_job_vram (775-786) : parse input_params meme si c'est un string JSON (important car asyncpg retourne les JSON comme strings). Default a 8000 MB si outil inconnu.SERVER_TOOLS (792) : {"ollama", "ollama-gpu1"} -- correct, ce sont les seuls outils qui gerent la concurrence en interne.resolve_tool_for_gpu (796-802) : remappe ollama<->ollama-gpu1 selon le GPU cible. Simple et correct.Points audites :
run_ssh_command (810-826) : voir F11 (injection SSH) et F19 (StrictHostKeyChecking).check_tool_health (829-842) : voir F24 (curl subprocess).check_port_listening (845-848) : utilise PowerShell Test-NetConnection. Fiable.get_all_gpu_status (851-907) : parse nvidia-smi via SSH. Le parsing par split sur , est fragile si nvidia-smi change son format de sortie. Double fallback (returncode != 0 et parse exception). Robuste.get_gpu_status (910-927) : appelle get_all_gpu_status pour un seul GPU. Pourrait etre optimise pour ne querier qu'un GPU, mais le cache rend ca acceptable._gpu_cache (930-942) : TTL 30s. Pas thread-safe (race condition possible si deux coroutines appellent simultanement apres expiration). En pratique, Python async est single-threaded donc pas de risque.stop_tool (945-977) : tue par port via PowerShell, puis execute le stop_cmd, puis supprime la scheduled task, puis verifie. Robuste.start_tool (980-1042) : voir F20 (backslash escaping). La logique de "stop tool on same GPU if different" est correcte. Le tool_start_lock (asyncio.Lock) empeche les starts concurrents.Points audites :
root (1047-1056) : expose WIN11_HOST et active_tools. OK pour un service interne.health (1059-1062) : retourne "healthy" meme si les GPUs sont down. C'est le comportement voulu (le container fonctionne, les jobs sont juste requeuees).worker_pause (1079-1104) : stop_tools=True par defaut. Itere tous les tools et check health avant de stopper. Correct.set_parallel_jobs (1133-1165) : voir F04 et F25.refresh_tools_status (1207-1243) : 14 health checks en parallele via asyncio.gather. Bon pattern. Reset active_tools atomiquement apres les checks.api_start_tool (1273-1293) : ne verifie pas si le worker est pause avant de demarrer un tool. C'est intentionnel (start manuel independant du worker).list_tools (1178-1189) : expose url: f"http://{WIN11_HOST}:{tool.port}" -- IP interne dans la reponse API. OK pour un service interne.Points audites :
db_create_job (1317-1330) : UUID4 pour job_id. json.dumps avec fallback default=str. Correct.db_list_jobs (1342-1372) : SQL dynamique avec parametres indexes ($1, $2...). Safe contre l'injection SQL. Exclut output_result pour la performance. Pagination avec COUNT + LIMIT/OFFSET.db_update_job_status (1401-1478) : voir F31. Le COALESCE(started_at, NOW()) est elegant (preserve le premier started_at).send_webhook_callback (1491-1535) : aiohttp importe localement (voir F22). Timeout 30s. Erreurs loguees mais pas relancees. Correct (les callbacks ne doivent pas bloquer les jobs).queue_push_job (1540-1550) : lpush (insert a gauche). queue_pop_job fait rpop (pop a droite). Cela donne un FIFO dans chaque queue de priorite. Correct.queue_remove_job (1563-1573) : parcourt toutes les queues pour trouver et supprimer un job. Race condition theorique si la queue est modifiee pendant le parcours, mais lrem est atomique.queue_peek_next_job (1576-1587) : utilise lindex(-1) pour peek sans pop. Fait un db_get_job pour chaque peek. Potentiellement lent si appele frequemment, mais n'est appele nulle part dans le code actuel (dead code ? Non, le worker utilise queue_pop_job directement).Points audites :
execute_bark_job (1675-1726) : Appelle Gradio API sur port 7866. Parse SSE pour extraire l'URL audio. Download et sauvegarde. Pattern solide.execute_musicgen_job (1729-1778) : Meme pattern que Bark. Timeout 300s pour la generation (raisonnable).execute_comfyui_job (1781-1955) : Construit un workflow ComfyUI complet en JSON. Soumet via POST, poll GET /history/{prompt_id} toutes les 2s pendant max 5 min. Download les images. Checkpoint hardcode (sd_xl_base_1.0.safetensors). Robuste.execute_fooocus_job (1958-2099) : Appelle Fooocus REST API. Gere base64, URL et file path. Voir F26 (seed 2^63). Modele hardcode (juggernautXL_v8Rundiffusion.safetensors). Si Fooocus n'est pas en mode API, retourne une erreur claire.Points audites :
execute_wan21_job (2102-2294) : Mode CLI via SSH, contourne Gradio. Prompt encode en base64 (safe, F11). Wrapper Python script ecrit sur win11 via PowerShell + base64. Timeout 45 min. Process-specific kill via wmic process where commandline like. Voir F30 (wrapper pas nettoye).execute_facefusion_job (2297-2490) : Mode CLI via SSH. Voir F11 (injection via face_swapper_model) et F21 (path traversal via source_file/target_file). Le cleanup temp files (rmdir /s /q) est fait en synchrone. Robust.Points audites :
keep_alive=-1 pour GPU1 (modele en memoire indefiniment). Correct.num_ctx bien present (ligne 2577-2588). Tool calls supportes. Resultat sauvegarde en fichier + retourne dans output_result. tokens_per_second calcule correctement (division-by-zero protegee par if eval_duration > 0).keep_alive (2520) : -1 pour GPU1 (port 11435), "30m" pour GPU0 (port 11434). Logique correcte : GPU1 est dedie Ollama, GPU0 est partage.Points audites :
_convert_ollama_to_anthropic (2705-2805) : gere system prompt, tool results, assistant tool_calls. cache_control: ephemeral ajoute sur le dernier tool et sur le system prompt. Pattern correct pour maximiser le cache Anthropic._convert_anthropic_to_ollama (2808-2852) : extraction correcte des content blocks (text + tool_use). Usage tokens bien extraits (cache_read, cache_creation).calculate_job_cost (2855-2879) : calcul correct. standard_input = max(0, tokens_in - cache_read - cache_create) -- note: les cache tokens sont deja comptes dans input_tokens par Anthropic, donc les soustraire est correct.check_anthropic_budget (2895-2905) : voir F06. Alert a 90% du budget (warning log).execute_anthropic_job (2908-2961) : shorthand model mapping ("fast"/"haiku"/"sonnet"/"default"). Timeout 120s pour l'API call (genereusement). Erreur propagee avec status_code.execute_anthropic_batch_job (2964-3065) : voir F10. Le poll toutes les 30s pendant max 2h. Log toutes les 5 min (poll i%10). Le parsing JSONL est correct (first line only pour notre single request). Le results_url est fourni par Anthropic et utilise directement.Points audites :
execute_applio_job (3068-3302) : voir F29 (60 params fragiles). Upload audio via FormData, refresh dropdowns, call enforce_terms, parse SSE. Pattern complexe mais bien commente. Le audio_dropdown = uploaded_audio_path.replace("\\", "\\\\") est un double-escaping qui pourrait poser probleme (meme logique que F20). Cependant, c'est pour le dropdown Gradio, pas pour SSH.execute_triposr_job (3305-3497) : Upload image, preprocess (remove background), generate, download mesh. 3 etapes Gradio API avec event_ids. SSE parsing avec async for line in resp.content. Gere les fallbacks URL (startswith "/", startswith "http", sinon "/file="). Robuste.execute_yolo_job (3500-3601) : Upload image via FormData au service YOLO FastAPI sur win11:5402. Parse le resultat structure (detections, classifications, keypoints, masks). Sauvegarde la visualisation (base64 -> fichier). Simple et propre. Voir F21 (path traversal via image_file).Points audites :
execute_sadtalker_job (3604-3835) : mode CLI via SSH. Voir F13 (zombie cleanup), F28 (job_id dans chemins). SFTP pour upload image/audio. Python wrapper script. Timeout 15 min. Find output video via dir /b *.mp4. Voir F21 (path traversal via source_image/driven_audio).determine_preferred_gpu (3847-3898) : Logique bien pensee:Points audites :
TOOL_TIMEOUT (3902-3916) : raisonnables. Wan2.1=1h, ComfyUI/Fooocus/FaceFusion=30min, Ollama=5min, Anthropic=5min. Note : le timeout Ollama (5 min) peut etre trop court pour des generations longues (qwen3:8b avec 8192 tokens de contexte). Le timeout interne dans execute_ollama_job est deja 5 min (ligne 2672), donc le double timeout est coherent.process_single_job (3918-4124) : Orchestration complete :job_worker (4126-4330) :gpu_vram_committed : max pour server tools, sum pour exclusive. Correct (mais voir F09 pour multi-model).can_dispatch : Exclusive running -> block. Exclusive new + server running -> block. Server concurrent limit. VRAM check.Points audites :
create_job (4335-4357) : valide tool_id, cree en DB, push queue. Retourne queue_position. Ne valide PAS les input_params (voir F11, F21).list_jobs (4360-4387) : pagination avec total. JSON serialization des dates et input_params. Appelle db_get_job_stats pour les stats globales (requete supplementaire par appel -- pourrait etre cache).get_job_status (4397-4419) : mode light qui strip la visualisation (utile pour le polling). Correct.cancel_job (4422-4441) : remove de la queue Redis + update DB. Race condition theorique : le job peut etre pop par le worker entre le check status et le remove. Le worker verifie status != "pending" (ligne 4261) donc le job ne sera pas execute. Correct.download_output (4444-4455) : path traversal check (.. et / dans filename). Correct. Mais ne verifie pas que job_id est un UUID valide. Un job_id comme ../../other/file permettrait de lire des fichiers hors de OUTPUTS_PATH. Cependant, job_id est toujours un UUID genere par uuid4() dans db_create_job, et le endpoint verifie que le job existe en DB (db_get_job n'est pas appele ici -- le endpoint ne verifie PAS que le job existe, il sert directement le fichier). Un attaquant pourrait donc tenter job_id=../../ mais .. n'est verifiable que dans filename, pas dans job_id. Ce vecteur est reellement exploitable.job_id est un parametre de route, pas filename. Le check .. est sur filename seulement. Un job_id de ../../etc est possible et file_path serait /mnt/stock_8to/33800-stack/ai-data/outputs/../../etc/passwd. Cependant, la route FastAPI {job_id} n'accepte que des segments de path sans /, donc ../../etc ne matcherait PAS la route. FastAPI interprete {job_id} comme un seul segment. Un job_id de .. matcherait, donnant file_path = /mnt/stock_8to/33800-stack/ai-data/outputs/../filename. C'est effectivement un path traversal d'un niveau. Mais le check ".." in filename or "/" in filename protege le filename, pas le job_id. Le job_id = ".." est possible. Cependant le fichier resultant serait dans ai-data/ pas outputs/, ce qui est un acces limite.job_id est genere par UUID en interne. Un attaquant devrait construire manuellement l'URL. Et meme avec .., il ne sort que d'un niveau. Les fichiers dans ai-data/ sont les inputs/outputs du meme service. Le nginx ne forwarde que le domaine ai-orchestrator, pas le filesystem.upload_input_file (4511-4537) : voir F12. os.path.basename(file.filename) + check ... Correct.delete_input_file (4575-4589) : check .. et /. os.remove(). Correct.costs_summary (4594-4634) : voir F15. SQL correct avec parametres.costs_live (4637-4669) : utilise CURRENT_DATE et date_trunc('month', CURRENT_TIMESTAMP). Dynamique, correct./api/queue/sync permet de recuperer d'un desync Redis/PostgreSQL... et / dans les noms de fichiers.json.dumps(value, default=str) evite les crashes sur les types non-serialisables (datetime, UUID, etc.).num_ctx: 8192 dans le mode generate d'Ollama (5 min, evite OOM) -- URGENCE: peut causer un OOM a tout momentprivate: true dans nginx conf (2 min, securise l'API) -- URGENT si le port forwarding est actifstartswith("/") dans les fonctions d'execution (15 min, corrige le path traversal dans 5 fonctions)/api/ps (1h)| 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 |