33800 Docs

← Retour

Audit approfondi ulias-org

Date : 15-02-2026 Status : IMPLEMENTEE Auditeur : Claude Opus 4.6 Projet : ulias-org (packages/server/src/) Methode : Lecture integrale de tous les fichiers source, tracage des chemins d'execution, verification croisee avec conf.prod.gouroubleu.yml


Resume executif

Le projet ulias-org est globalement bien structure : architecture modulaire (director/interpreter/agent-runner/briefing/db), separation des responsabilites, gestion d'erreurs non-bloquante pour la DB. Cependant, l'audit revele 3 bugs certains, 5 bugs probables, 4 risques theoriques, et 2 faux positifs parmi les findings signales lors du premier audit.


1. BUGS CERTAINS

1.1 Briefing processAnswer — Logique de skip inversee

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/briefing/index.ts lignes 148-151 Severite : HAUTE

const skipWords = ['passer', 'skip', '-', 'non', 'rien', 'aucun'];
if (!currentQ.required || !skipWords.includes(answer.trim().toLowerCase())) {
  session.answers.set(currentQ.id, answer);
}

Analyse detaillee :

La condition !currentQ.required || !skipWords.includes(answer) se traduit par :

Le bug : Pour les questions required: false, l'utilisateur peut dire "passer" mais sa reponse sera quand meme enregistree comme "passer" dans les answers. Le skip est mort pour les questions optionnelles.

La logique correcte serait :

const isSkip = skipWords.includes(answer.trim().toLowerCase());
if (!isSkip || currentQ.required) {
  session.answers.set(currentQ.id, answer);
}

Qui signifie : "enregistre la reponse sauf si c'est un skip ET que la question n'est pas obligatoire".

Impact : Quand l'utilisateur repond "passer" ou "non" a une question de briefing optionnelle (ex: "Autres details ?"), la valeur "passer" est stockee et passee au LLM comme specification. Cela peut polluer les prompts enrichis et generer du code avec des champs "passer".

Classification : BUG CERTAIN


1.2 CLI default server URL pointe vers le mauvais port

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/cli/bin/ulias.ts ligne 13 Severite : MOYENNE

const DEFAULT_SERVER = process.env.ULIAS_SERVER_URL || 'ws://localhost:5510/ws/cli';

Le serveur ulias-org ecoute sur le port 5515 (confirme dans index.ts ligne 17 et conf.prod.gouroubleu.yml ligne 6). Le port 5510 est celui de claude-memory (d'apres CLAUDE.md).

Impact : Le CLI ne peut pas se connecter sans specifier explicitement --server ws://localhost:5515/ws/cli ou via ULIAS_SERVER_URL. Le mode par defaut est casse.

Classification : BUG CERTAIN


1.3 WS deconnexion — crash potentiel sur ws.send apres fermeture

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/index.ts lignes 86, 130, 222-228, 294, 351 Severite : MOYENNE

Plusieurs chemins appellent ws.send() sans verifier l'etat du WebSocket :

Chemin 1generateConversationTitle (ligne 86) :

ws.send(JSON.stringify({
  type: 'conversation_title_updated',
  conversationId: convId,
  title,
  tags,
}));

Cette fonction est fire-and-forget (ligne 386). Si le client se deconnecte entre le debut du traitement et la fin de la generation du titre (~30s de LLM), ws.send() crash.

Chemin 2generateConversationSummary (ligne 127) :

if (ws) {
  ws.send(JSON.stringify({ ... }));
}

Le guard if (ws) verifie que ws est truthy mais pas que le WS est encore ouvert.

Chemin 3onEvent callback (ligne 294) :

const onEvent = (event: DirectorEvent) => {
  ws.send(JSON.stringify(event));

Ce callback est appele pendant toute l'execution d'un objectif (potentiellement 10 iterations x 5 min = plusieurs minutes). Si le client se deconnecte pendant, chaque event enverra un ws.send sur un socket ferme.

Note sur Elysia/Bun WS : Dans Bun, ws.send() sur un WebSocket ferme lance une exception. Le catch global (ligne 426) devrait attraper l'erreur pour le handler handleWsMessage, mais les fonctions fire-and-forget (generateConversationTitle, generateConversationSummary) ont leur propre try/catch qui log mais ne nettoie pas la session.

Impact : Logs pollues par des erreurs de send apres deconnexion. Pas de data loss grace aux catch, mais nuisance operationnelle.

Classification : BUG CERTAIN (se produit a chaque deconnexion pendant un traitement)


2. BUGS PROBABLES

2.1 CALLBACK_BASE_URL hardcode avec mauvaise IP pour Docker

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/tools/client.ts ligne 112 Severite : HAUTE

const CALLBACK_BASE = process.env.CALLBACK_BASE_URL || 'http://192.168.1.12:5515';

Ce fallback pointe vers 192.168.1.12:5515 (prod-portainer). Comme le container tourne effectivement sur prod-portainer, cela devrait fonctionner pour le trafic intra-machine.

Cependant : conf.prod.gouroubleu.yml ne definit PAS CALLBACK_BASE_URL dans les env vars (seuls CONNECTORS_API_URL, GITLAB_URL, GITLAB_TOKEN sont definis). Donc le fallback hardcode est utilise en prod.

Le probleme : Si ai-orchestrator tourne sur win11 (192.168.1.30) et doit appeler le callback sur ulias-org (192.168.1.12:5515), il doit pouvoir atteindre cette IP. C'est probable que ca fonctionne (meme VLAN), mais la communication depasse le loopback.

Verification : L'env var n'est pas injectee donc le fallback est actif. Le callback semble fonctionner en pratique (sinon tout le LLM serait casse), donc le 192.168.1.12:5515 est probablement joignable depuis win11.

Classification : BUG PROBABLE — fonctionne en pratique mais fragile : tout changement d'IP ou de port cassera les callbacks sans warning.


2.2 CONNECTORS_API_URL fallback sur port 5400 (pas 5403)

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/index.ts ligne 16 Severite : BASSE (en prod, l'env var est injectee)

const CONNECTORS_API_URL = process.env.CONNECTORS_API_URL || 'http://192.168.1.12:5400';

Le fallback est 5400, mais conf.prod.gouroubleu.yml injecte CONNECTORS_API_URL: "http://192.168.1.12:5403".

Verification : En production, l'env var est correctement injectee via smart-deploy. Le fallback 5400 ne serait utilise qu'en dev local sans .env, et dans ce cas ce serait effectivement une erreur.

Classification : BUG PROBABLE — ne touche pas la prod (env var injectee), mais piege en dev local.


2.3 Response cache hash trop faible — collisions possibles

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/tools/response-cache.ts lignes 23-37 Severite : BASSE

static hashKey(messages: any[], model?: string): string {
  const userMsgs = messages.filter((m: any) => m.role === 'user');
  const lastUser = userMsgs[userMsgs.length - 1]?.content || '';
  const systemPrompt = messages.find((m: any) => m.role === 'system')?.content || '';
  const raw = `${model || 'default'}::${systemPrompt.slice(0, 200)}::${lastUser}`;
  let hash = 0;
  for (let i = 0; i < raw.length; i++) {
    const chr = raw.charCodeAt(i);
    hash = ((hash << 5) - hash) + chr;
    hash |= 0;
  }
  return `cache_${hash}`;
}

Problemes :

  1. Le hash est un DJB2 32-bit — seulement ~4 milliards de valeurs possibles. Avec 50 entrees max ce n'est probablement pas un probleme statistique.
  2. Le system prompt est tronque a 200 chars. Deux agents avec des prompts qui different apres 200 chars auraient le meme hash si le user message est identique.
  3. Les messages tool_calls/tool_results intermediaires sont ignores — le cache ne s'utilise que sur la premiere iteration (iterations === 1 dans agent-runner), ce qui est correct.

Impact reel : Le cache TTL est de 5 min et la taille max est 50. Les collisions sont improbables en pratique car le systemPrompt des 200 premiers chars est generalement distinctif entre agents.

Classification : BUG PROBABLE — risque faible de collision en pratique, mais le design est fragile.


2.4 message_count increment non-atomique

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/index.ts lignes 373-376 Severite : BASSE

const conv = await db.getConversation(convId);
const prevCount = conv?.message_count || 0;
await db.updateConversation(convId, {
  last_message_at: new Date().toISOString(),
  message_count: prevCount + 2,

Pattern read-then-write classique. Si deux messages arrivent quasi-simultanement (par ex. via deux clients WS sur la meme conversation), prevCount peut etre lu a la meme valeur et le deuxieme ecrase le compteur du premier.

Impact : En mono-utilisateur pratique, le risque est tres faible. Le message_count est utilise pour declencher l'auto-titre (a 0) et l'auto-resume (tous les 10). Un increment perdu ne causerait qu'un retard dans le declenchement du resume.

Classification : BUG PROBABLE — race condition reelle, impact minimal en mono-utilisateur.


2.5 NotificationService instanciee mais jamais utilisee

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/notifications/index.ts Severite : BASSE

La classe NotificationService est bien definie avec des methodes objectiveCompleted, objectiveFailed, decisionNeeded, proactiveAlert. Cependant, elle n'est importee nulle part dans le code du serveur.

$ grep -r "NotificationService" packages/server/src/
→ Seulement dans notifications/index.ts

Le serveur envoie des events via WS directement dans handleWsMessage et des notifications via toolsClient.notify() dans l'agent-runner. Le service de notification centralise est du code mort.

Classification : BUG PROBABLE — code mort, pas de regression, mais indique une architecture inachevee.


3. RISQUES THEORIQUES

3.1 /api/agents expose les prompts sans authentification

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/index.ts lignes 775-788 Severite : BASSE

.get('/api/agents', async ({ headers }) => {
  const apiKey = headers['x-api-key'];
  const agents = getAllAgents();
  return Object.entries(agents).map(([name, a]) => ({
    name,
    team: a.team,
    description: a.description,
    model: a.model,
    prompt: a.prompt,             // ← Prompt complet expose
    defaultPrompt: getDefaultPrompt(name),
    hasOverride: hasPromptOverride(name),
    requiresApproval: a.requiresApproval,
  }));
})

Le header apiKey est lu mais jamais verifie. getAllAgents() est appele inconditionnellement. N'importe qui peut GET /api/agents et obtenir tous les prompts systeme.

Impact reel : Les prompts ne contiennent pas de secrets (pas de tokens, pas de passwords). Ils contiennent des informations d'architecture (IPs internes, noms de machines, ports) qui sont dans les prompts des agents. Cependant :

Verification : conf.prod.gouroubleu.yml ne specifie pas private: true dans la section nginx. Donc le endpoint est public sur Internet si quelqu'un connait le domaine ulias-org.33800.nowhere84.com.

Classification : RISQUE THEORIQUE — les prompts contiennent des IPs internes et des noms de machines mais pas de credentials. Un attaquant externe aurait besoin de connaitre le domaine exact.


3.2 GITLAB_TOKEN en clair dans conf.prod.gouroubleu.yml

Fichier : /stock_8to/33800-stack/projects/ulias-org/conf.prod.gouroubleu.yml ligne 10 Severite : MOYENNE

env:
  GITLAB_TOKEN: "glpat-yaowLwWBJhXfzJEC8UBC"

Le token GitLab personnel est en clair dans le fichier de configuration. Ce fichier est versionne dans le depot GitLab ulias-org.

Verification : Ce token apparait aussi dans CLAUDE.md. Il est utilise par les agents pour les operations GitLab (create project, create file, etc.).

Impact : Toute personne ayant acces au repo GitLab a le token admin. C'est le meme token pour tout.

Note : Ce token est deja connu dans CLAUDE.md et utilise partout dans l'infra. Le risque est surtout d'exposition accidentelle si le repo est rendu public.

Classification : RISQUE THEORIQUE — dans un contexte prive (GitLab self-hosted, single user), le risque est limite. Cependant, la bonne pratique serait d'utiliser des secrets Docker ou une variable d'environnement non versionnee.


3.3 Injection de commande via SSH exec

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/tools/client.ts lignes 176-202 Severite : BASSE (contexte mono-utilisateur)

async exec(instance: string, command: string): Promise<ExecResult> {
  const inst = this.userCtx.instances.find(...);
  const res = await fetch(`${this.connectorsUrl}/api/ssh/execute`, {
    body: JSON.stringify({ instance_id: inst.id, command }),
  });

Les commandes SSH sont passees telles quelles. Les agents LLM construisent ces commandes a partir des inputs utilisateur et de leur raisonnement.

Mais : Le seul chemin pour envoyer des commandes est via les agents LLM, qui sont declenches par des messages WS authentifies (API key). L'utilisateur est aussi le proprietaire des machines SSH. C'est de l'execution de commande "by design".

async writeFile(instance: string, path: string, content: string): Promise<void> {
  const escaped = content.replace(/'/g, "'\\''");
  const result = await this.exec(instance, `cat > "${path}" << 'ULIAS_EOF'\n${escaped}\nULIAS_EOF`);

Le writeFile echappe les single quotes dans le heredoc. Si le contenu contient ULIAS_EOF en debut de ligne, le heredoc se termine prematurement et le reste du contenu est interprete comme une commande shell.

Classification : RISQUE THEORIQUE — en mono-utilisateur, l'utilisateur execute ses propres commandes. Le risque d'injection via ULIAS_EOF dans le contenu est reel mais improbable en pratique (quel fichier contiendrait ce pattern?).


3.4 Pas de validation de taille sur les inputs WebSocket

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/index.ts lignes 213-428 Severite : BASSE

Le handler handleWsMessage ne verifie pas la taille du message entrant. Un client malveillant pourrait envoyer un message de plusieurs MB qui serait parse, traite, et passe au LLM.

Impact : En mono-utilisateur authentifie, c'est un self-DOS. En pratique, le LLM a un num_ctx de 16384 tokens et le context-builder tronque les inputs.

Classification : RISQUE THEORIQUE


4. FAUX POSITIFS (findings du premier audit a corriger)

4.1 "Endpoints sans codes HTTP d'erreur" — FAUX POSITIF

Tous les endpoints REST retournent { error: 'message' } sans setter set.status. Elysia retourne automatiquement un 200 meme en cas d'erreur applicative.

Cependant : C'est un pattern courant dans les APIs internes. Le frontend et le CLI ne verifient que le champ error dans le JSON, pas le status HTTP. Ce n'est pas un bug — c'est un choix architectural. Les seuls endpoints qui pourraient beneficier d'un 401 sont les endpoints non-authentifies, mais le front gere deja le cas via le champ error.

Classification : FAUX POSITIF pour un bug. C'est une amelioration potentielle, pas un defaut fonctionnel.


4.2 CONNECTORS_API_URL port 5400 vs 5403 en prod — FAUX POSITIF en prod

Comme verifie au point 2.2, l'env var est correctement injectee en prod via conf.prod.gouroubleu.yml : CONNECTORS_API_URL: "http://192.168.1.12:5403". Le fallback 5400 n'est jamais utilise en production.

Classification : FAUX POSITIF pour la prod. Bug en dev local seulement (voir 2.2).


5. PROBLEMES ARCHITECTURAUX (non-bugs)

5.1 Prompt overrides partages entre sessions

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/agents/registry.ts lignes 640-655

const promptOverrides = new Map<string, string>();

export function setPromptOverride(agentName: string, prompt: string): void {
  promptOverrides.set(agentName, prompt);
}

Les overrides de prompts sont stockes dans une variable globale (promptOverrides) au niveau du module. Si deux sessions differentes chargent des overrides differents, la derniere session ecrase les overrides de la premiere.

En pratique mono-utilisateur, ce n'est pas un probleme. Mais c'est une dette technique pour le multi-tenant.

5.2 Session cleanup ne deconnecte pas le WS

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/index.ts lignes 197-210

setInterval(() => {
  const now = Date.now();
  for (const [key, session] of sessions) {
    if (now - session.lastActivity > SESSION_IDLE_MS) {
      sessions.delete(key);
      suggestionsCache.delete(session.userCtx.userId);
    }
  }
}, CLEANUP_INTERVAL_MS);

Quand une session idle est supprimee, le WS associe (session.ws) n'est pas ferme. Le client reste connecte mais sa session est detruite. Le prochain message WS echouera avec "Not authenticated".

Ce n'est pas un crash (le front gere l'erreur), mais l'UX serait meilleure si on fermait le WS avec un code et un message explicite.

5.3 Traduction automatique de chaque reponse

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/director/index.ts lignes 666-684

private async translateOutput(output: string): Promise<string> {
  if (output.length < 20) return output;
  try {
    const response = await this.toolsClient.llmChat(
      [
        { role: 'system', content: 'Tu es un traducteur...' },
        { role: 'user', content: output },
      ],
      { model: 'qwen3:8b', temperature: 0, num_ctx: 4096 }
    );

Chaque reponse agent passe par un appel LLM supplementaire pour traduction, meme si elle est deja en francais. Le prompt dit "Si le texte est deja en francais, renvoie-le tel quel", mais cela consomme quand meme un job GPU, ajoute de la latence (~5-10s), et risque de deformer la reponse (le LLM peut "traduire" du code ou des commandes).

C'est un choix delibere, pas un bug, mais l'impact en performance est significatif : chaque requete utilisateur genere au minimum 3 appels LLM (routing + agent + traduction) au lieu de 2.

5.4 fetchRelevantLessons appele a chaque tache

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/director/index.ts lignes 690-704

private async fetchRelevantLessons(message: string): Promise<string[]> {
  try {
    const embedding = await this.toolsClient.llmEmbed(message);
    const lessons = await this.db.getRelevantLessons(this.userCtx.userId, embedding, 5);

A chaque tache, un embedding est genere (1 appel LLM) et 50 lessons sont recuperees de la DB puis classees cote application. Pour une requete multi-step avec 3 sous-taches, cela fait 3 embeddings + 3 queries DB de 50 rows chacune.

Ce n'est pas un bug, mais c'est une source de latence.


6. SECURITE

6.1 Endpoints internes sans authentification

Fichiers : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/index.ts lignes 1195-1209

.post('/internal/job-callback/:callbackId', async ({ params, body }) => {
  const { callbackId } = params;
  const resolved = resolveJobCallback(callbackId, body as any);
  return { ok: true };
})

.get('/internal/pending-jobs', () => {
  return { pending: getPendingJobIds() };
})

Les endpoints /internal/* ne verifient aucune authentification. N'importe qui pouvant atteindre le port 5515 peut :

  1. Resoudre un callback arbitraire avec un resultat forge (si le callbackId est un UUID valide)
  2. Lister les job IDs en attente

Evaluation :

Classification : RISQUE THEORIQUE — l'UUID rend l'exploitation improbable, mais l'absence d'auth est un mauvais pattern.

6.2 API key stockee dans Supabase en clair

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/auth.ts lignes 160-198

async function storeApiKey(...): Promise<void> {
  const prefs = { ...(profile.preferences || {}), connector_api_key: apiKey };
  await fetch(`${connectorsUrl}/api/fetch`, {
    body: JSON.stringify({
      connector: 'supabase',
      method: 'PATCH',
      path: `/rest/v1/ceo_profile?id=eq.${profile.id}`,
      body: { preferences: prefs },
    }),
  });

L'API key connectors-api est stockee en clair dans la table agents.ceo_profile, colonne preferences, champ connector_api_key. Toute personne ayant acces a Supabase (meme en lecture seule) peut recuperer l'API key.

Classification : RISQUE THEORIQUE — en self-hosted mono-utilisateur, le risque est limite. En multi-tenant, ce serait critique.

6.3 Endpoint DELETE /api/objectives/:id sans verification d'ownership

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/index.ts lignes 515-535

.delete('/api/objectives/:id', async ({ headers, params }) => {
  const apiKey = headers['x-api-key'];
  const session = await resolveSession(apiKey);
  if (!session) return { error: 'Not authenticated' };

  try {
    const db = new AgentsDB(session.toolsClient);
    const obj = await db.getObjective(params.id);
    if (!obj) return { error: 'Objective not found' };
    // ← Pas de verification obj.user_id === session.userCtx.userId
    await db.deleteObjective(params.id);

Idem pour POST /api/objectives/:id/cancel, GET /api/objectives/:id, et DELETE /api/conversations/:id. L'authentification est verifiee mais l'ownership de l'objet ne l'est pas. Un utilisateur authentifie pourrait supprimer les objectifs d'un autre utilisateur.

Classification : RISQUE THEORIQUE — en mono-utilisateur, pas d'impact. En multi-tenant, ce serait une vulnerabilite IDOR.


7. QUALITE DE CODE

7.1 num_ctx manquant dans certains appels llmChat

Observation : Le translateOutput (director, ligne 675) passe num_ctx: 4096 dans un objet LLMOptions. Mais LLMOptions interface (client.ts) n'a pas de champ num_ctx. Ce champ est ajoute par la methode llmChat elle-meme (ligne 324: inputParams.num_ctx = 16384).

Le num_ctx: 4096 passe par translateOutput est un champ inconnu dans LLMOptions et sera ignore silencieusement par le spread. Le LLM recevra toujours num_ctx: 16384.

Ce n'est pas un crash mais le code pense passer 4096 alors qu'il passe 16384. Ce n'est pas grave (16384 suffit) mais c'est trompeur.

7.2 Pattern getAgent(name)! avec null assertion

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/agents/interpreter.ts lignes 321, 327, 333

const agentDef = getAgent('monitor')!;

Utilise la non-null assertion ! sans verification. Si un agent est renomme ou supprime du registre, le runtime crashera. Le code du director (ligne 591-601) gere correctement ce cas avec un fallback vers explorer.

7.3 Types any frequents

De nombreux endroits utilisent body as any, query as any, etc. C'est un compromis Elysia (pas de validation de schema declaree). Non critique mais augmente le risque de bugs sur les inputs.


8. SYNTHESE PAR PRIORITE

# Finding Classification Severite Action
1.1 Briefing processAnswer logique inversee BUG CERTAIN HAUTE Corriger la condition
1.2 CLI port 5510 au lieu de 5515 BUG CERTAIN MOYENNE Changer le port default
1.3 ws.send apres deconnexion BUG CERTAIN MOYENNE Ajouter guard readyState
2.1 CALLBACK_BASE_URL hardcode sans env BUG PROBABLE HAUTE Ajouter dans conf.prod
2.2 CONNECTORS_API_URL fallback 5400 BUG PROBABLE BASSE Corriger fallback ou supprimer
2.3 Cache hash faible BUG PROBABLE BASSE Ameliorer le hash
2.4 message_count non-atomique BUG PROBABLE BASSE Utiliser increment SQL
2.5 NotificationService inutilisee BUG PROBABLE BASSE Integrer ou supprimer
3.1 /api/agents sans auth RISQUE THEORIQUE BASSE Ajouter auth check
3.2 GITLAB_TOKEN en clair RISQUE THEORIQUE MOYENNE Utiliser Docker secrets
3.3 Injection heredoc writeFile RISQUE THEORIQUE BASSE Utiliser un delimiteur unique
3.4 Pas de limite taille WS RISQUE THEORIQUE BASSE Ajouter maxPayloadLength
4.1 HTTP status codes FAUX POSITIF
4.2 Port 5400 en prod FAUX POSITIF

9. RECOMMANDATIONS (par ordre de priorite)

  1. Corriger le bug briefing (1.1) — Impact direct sur l'UX du workflow de creation de projet
  2. Ajouter CALLBACK_BASE_URL dans conf.prod.gouroubleu.yml (2.1) — Rendre explicite ce qui est implicite
  3. Corriger le port CLI (1.2) — De 5510 a 5515
  4. Ajouter un guard ws.send (1.3) — Verifier ws.readyState === 1 avant chaque send
  5. Deplacer GITLAB_TOKEN vers un secret Docker (3.2) — Bonne pratique securite
  6. Ajouter auth sur /api/agents (3.1) — Verification triviale a ajouter
  7. Optimiser la traduction (5.3) — Detecter la langue avant de traduire, ou cacher le resultat
  8. Integrer ou supprimer NotificationService (2.5) — Nettoyer le code mort

10. COMPLEMENT D'AUDIT — Fichiers non couverts

Ajout 15-02-2026 : L'audit initial couvrait principalement index.ts, briefing/index.ts, director/index.ts, agents/registry.ts, tools/client.ts, tools/response-cache.ts, notifications/index.ts, auth.ts, et le CLI. Les fichiers suivants n'avaient PAS ete audites ou l'avaient ete superficiellement :

  • context-builder/index.ts
  • tools/git-workflow.ts
  • tools/circuit-breaker.ts
  • tools/definitions.ts
  • pipelines/index.ts
  • agent-runner/index.ts (mentionne mais pas audite en profondeur)
  • db/index.ts (mentionne mais pas audite en profondeur)
  • .env.deploy
  • Dockerfile
  • conf.prod.gouroubleu.yml (partiellement couvert)

10.1 context-builder/index.ts — Estimation token incorrecte

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/context-builder/index.ts lignes 23-25 Severite : BASSE

const TOKEN_BUDGET = 6000;
const CHARS_PER_TOKEN = 3.5;
const MAX_CHARS = TOKEN_BUDGET * CHARS_PER_TOKEN; // ~21000 chars

Le ratio 3.5 chars/token est approximatif. Pour du texte technique (noms de variables, IPs, chemins), le ratio reel est plutot ~4-5 chars/token. Pour du francais, c'est plutot 2.5-3 chars/token (les diacritiques comptent plus).

Impact : Le budget est fixe a ~21000 chars, mais avec num_ctx: 16384 dans llmChat (client.ts ligne 324), le vrai budget en tokens est de ~16384 - messages_overhead. L'approximation sous-estime la taille reelle du prompt en tokens pour le texte technique, ce qui pourrait causer des troncatures non prevues par le LLM (messages tronques au milieu).

En pratique, le num_ctx hardcode a 16384 est le vrai garde-fou. Le context-builder sert de pre-filtre cote application. Le risque est faible car les layers fixes (rules + state) sont rarement >6000 chars.

Classification : RISQUE THEORIQUE — le budget est approximatif mais fonctionne comme pre-filtre grossier.


10.2 context-builder/index.ts — .sort() mutable sur le tableau layers

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/context-builder/index.ts lignes 81, 116-117 Severite : BASSE

private assembleWithTrimming(layers: ContextLayer[]): string {
  layers.sort((a, b) => a.priority - b.priority); // mute l'array en place
  // ...
  return [...fixedLayers, ...trimmedLayers]
    .sort((a, b) => a.priority - b.priority) // re-sort
    .map(l => l.content)
    .join('\n\n');
}

Le premier .sort() mute le tableau layers recu en parametre. Ce n'est pas un useStore Qwik donc pas de boucle infinie, mais c'est un anti-pattern : le caller pourrait ne pas s'attendre a ce que son tableau soit mute. La methode build() cree le tableau localement, donc pas de side-effect en pratique.

Classification : QUALITE DE CODE — pas de bug, mais le spread avant sort serait plus propre.


10.3 tools/git-workflow.ts — Status code '??' teste deux fois, 'untracked' jamais atteint

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/tools/git-workflow.ts lignes 31-37 Severite : BASSE

let status: ChangedFile['status'] = 'modified';
if (statusCode === '??' || statusCode === 'A') status = 'added';
else if (statusCode === 'D') status = 'deleted';
else if (statusCode === 'R') status = 'renamed';
else if (statusCode === '??') status = 'untracked';

Le premier if teste deja statusCode === '??' et assigne 'added'. Le dernier else if teste a nouveau '??' mais ne sera JAMAIS atteint (court-circuit par le premier if). Les fichiers non-trackes (??) seront donc classes comme 'added' au lieu de 'untracked'.

Le type ChangedFile['status'] inclut 'untracked' comme valeur possible, mais cette valeur n'est jamais assignee.

Impact : Fonctionnel mais semantiquement faux. Si un agent utilise le status 'untracked' pour decider quoi stager (ex: ignorer les fichiers non-trackes), il ne les detectera jamais car ils sont classes comme 'added'.

Classification : BUG CERTAIN — logique mort, 'untracked' n'est jamais assigne. Le fix serait d'inverser l'ordre (tester '??' pour untracked en premier, puis 'A' pour added).


10.4 tools/git-workflow.ts — Non utilise dans le code principal

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/tools/git-workflow.ts Severite : BASSE

Ce module exporte 4 fonctions (getChangedFiles, ensureCleanWorkingTree, createBranchAndCommit, getCurrentBranch) mais aucune n'est importee dans le reste du codebase serveur.

Verification :

grep -r "git-workflow" packages/server/src/ → aucun resultat
grep -r "getChangedFiles\|ensureCleanWorkingTree\|createBranchAndCommit\|getCurrentBranch" packages/server/src/ → aucun resultat hors git-workflow.ts

Les operations git des agents passent directement par ToolsClient.gitCommit(), ToolsClient.exec(), etc. Le module git-workflow est du code mort prepare pour un usage futur.

Classification : CODE MORT — pas de regression, mais augmente la surface de maintenance.


10.5 tools/circuit-breaker.ts — Etat global partage entre agents

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/tools/circuit-breaker.ts Fichier d'import : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/agent-runner/index.ts ligne 13

const toolCircuitBreaker = new CircuitBreaker(3, 60000);

Le circuit breaker est instancie une fois au niveau du module agent-runner. Il est partage entre TOUS les agents et TOUTES les sessions. Si un agent "coder" fait echouer l'outil exec 3 fois de suite (ex: SSH timeout), le circuit s'ouvre et TOUS les agents de TOUTES les sessions se voient refuser exec pendant 60 secondes.

Impact : En mono-utilisateur, c'est plutot un feature (empeche les cascades). En multi-agent concurrent, un agent qui boucle sur un tool defaillant peut bloquer les autres agents.

Classification : RISQUE THEORIQUE — correct pour mono-utilisateur, pourrait poser probleme en multi-tenant.


10.6 tools/definitions.ts — Tool set 'storage-dev' pointe vers 'infra'

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/agents/registry.ts ligne 635 Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/tools/definitions.ts lignes 658-692

// registry.ts:
'storage-dev': {
  // ...
  toolSet: 'infra',  // ← pointe vers le toolset 'infra', PAS vers 'storage-dev'
}

L'agent storage-dev a toolSet: 'infra', ce qui lui donne les outils [execTool, dockerPsTool, dockerRestartTool, notifyTool]. Il n'a PAS les outils storageXxx definis dans TOOL_SETS['storage-dev'].

Cependant, le prompt de storage-dev (registry.ts lignes 598-637) dit explicitement : "ACCESS METHOD: Use exec() on prod-portainer-SSH to run curl commands directly. Do NOT use the storageXxx tools — they have auth issues."

Verification : Le TOOL_SETS['storage-dev'] existe et contient [storageListBucketsTool, storageCreateBucketTool, ...]. Mais l'agent storage-dev utilise toolSet: 'infra' intentionnellement car les outils storage ont des problemes d'auth (comme documente dans le prompt).

Classification : CHOIX DELIBERE — pas un bug, mais confusant. Le TOOL_SETS['storage-dev'] est defini mais jamais utilise par l'agent storage-dev. Les tools storage y sont car ils sont potentiellement utilises par d'autres pipelines (mais en verifiant les TOOL_SETS, seul 'storage-dev' key les contient, et aucun agent ne l'utilise).

Consequence reelle : Les 7 tools de TOOL_SETS['storage-dev'] (storageListBuckets, storageCreateBucket, storageListFiles, storageUpload, storageDelete, storageGetPublicUrl) sont du code mort — definis mais jamais passes a un agent.


10.7 tools/client.ts — storageGetPublicUrl avec IP hardcodee en fallback

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/tools/client.ts ligne 637

async storageGetPublicUrl(bucket: string, path: string): Promise<string> {
  const sbInstance = this.userCtx.instanceMap.supabase;
  if (!sbInstance) throw new Error('No Supabase instance configured');
  const baseUrl = sbInstance.config?.url || 'http://192.168.1.12:8200';
  return `${baseUrl}/storage/v1/object/public/${bucket}/${path}`;
}

Le fallback http://192.168.1.12:8200 est une IP hardcodee. Selon les regles CLAUDE.md (regle 17), les IPs hardcodees dans le code sont a eviter car Pi-hole split-DNS gere la resolution. L'URL correcte serait https://supabase.33800.nowhere84.com.

Impact : Si sbInstance.config?.url est undefined (ce qui depend de la config du connecteur Supabase dans connectors-api), l'URL generee sera en HTTP sur une IP interne, ce qui :

  1. Ne fonctionnera pas depuis l'exterieur du LAN
  2. Sera en HTTP au lieu de HTTPS
  3. Cassera si l'IP change

Note : Ce code est dans un chemin mort car storageGetPublicUrl n'est appelee que via le tool storageGetPublicUrl dans agent-runner/index.ts ligne 594, et l'agent storage-dev n'a pas ce tool (voir 10.6). Seul index.ts ligne 895 appelle directement session.toolsClient.storageGetPublicUrl() dans le endpoint /api/upload.

Classification : BUG PROBABLE — IP hardcodee dans le code applicatif, violation de la regle Pi-hole. Impact reel limite car le chemin est rarement utilise.


10.8 tools/client.ts — editFile ne remplace que la premiere occurrence

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/tools/client.ts lignes 220-233

async editFile(instance: string, path: string, oldString: string, newString: string): Promise<void> {
  const content = await this.readFile(instance, path);
  if (!content.includes(oldString)) {
    throw new Error(`editFile: old_string not found in ${path}`);
  }
  const newContent = content.replace(oldString, newString); // ← String.replace sans /g
  await this.writeFile(instance, path, newContent);
}

String.replace(string, string) ne remplace que la premiere occurrence. Si le LLM-agent veut remplacer un pattern qui apparait plusieurs fois (ex: renommer une variable), seule la premiere sera modifiee.

Ce comportement est identique a celui de Claude Code (qui remplace la premiere occurrence pour eviter les effets de bord), donc c'est probablement intentionnel. Mais la description du tool (editFile dans definitions.ts ligne 63) dit juste "replace old_string with new_string" sans preciser "first occurrence only".

Classification : QUALITE DE CODE — probablement intentionnel mais le comportement devrait etre documente dans la tool description pour que le LLM sache qu'il doit faire plusieurs appels si le pattern apparait plusieurs fois.


10.9 agent-runner/index.ts — toolCallsCount non-thread-safe avec parallel execution

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/agent-runner/index.ts lignes 193-195

const parallelResults = await Promise.all(
  canParallel.map(async (toolCall) => {
    toolCallsCount++; // ← increment dans une Promise.all

toolCallsCount++ est execute dans des callbacks concurrents de Promise.all. En JavaScript single-thread, l'increment est atomique car il n'y a pas de preemption entre les ++. Cependant, si executeTool() est synchrone pour certains outils (retour immediat), les increments sont effectivement sequentiels. Si executeTool() contient des await, les callbacks reprennent dans un ordre non-deterministe, mais ++ reste atomique en single-thread.

Classification : FAUX POSITIF — JavaScript single-thread garantit l'atomicite de ++. Pas de race condition possible.


10.10 agent-runner/index.ts — compactToolResult fait un appel LLM supplementaire pour les gros resultats

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/agent-runner/index.ts lignes 460-476

// LLM summarization for large results
try {
  const summary = await this.toolsClient.llmChat(
    [
      { role: 'system', content: 'Tu es un assistant qui resume des donnees...' },
      { role: 'user', content: `Resume ces donnees (${slimStr.length} chars):\n${slimStr.slice(0, 12000)}` },
    ],
    { model: 'qwen3:8b', temperature: 0, num_ctx: 16384, timeout: 30000 }
  );

Quand un outil retourne un resultat >6000 chars et que le field-filtering ne suffit pas, compactToolResult fait un appel LLM supplementaire pour resumer. Cela ajoute :

C'est un choix architectural documente mais non mentionne dans l'audit initial. Combine avec la traduction (section 5.3) et le routing (section 5.4), une seule requete utilisateur peut declencher : 1 routing + 1 embedding + N agent iterations + M summarizations + 1 traduction = potentiellement >10 appels LLM.

Note : num_ctx: 16384 est passe directement dans les options ici, ce qui est la bonne valeur. Contrairement a translateOutput (finding 7.1) ou num_ctx: 4096 est ignore, ici le num_ctx n'est pas dans LLMOptions mais est passe comme extra param — il sera spread dans inputParams via ...options dans llmChat. Verification : en fait non, llmChat ne spread PAS les options non reconnues. Il lit options.model, options.temperature, options.tools, options.gpu, options.priority, options.maxTokens, options.timeout, options.provider — c'est tout. Le num_ctx passe ici est egalement ignore, tout comme dans translateOutput.

Correction du finding : compactToolResult a le meme probleme que translateOutput (7.1) : le num_ctx passe dans les options est silencieusement ignore. Le num_ctx est toujours 16384 (hardcode dans llmChat ligne 324).

Classification : PROBLEME ARCHITECTURAL (latence) + BUG PROBABLE (num_ctx ignore, meme probleme que 7.1)


10.11 agent-runner/index.ts — consecutiveErrors partage entre parallel tools

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/agent-runner/index.ts lignes 101-102, 210-222

let consecutiveErrors = 0;
let lastErrorMsg = '';
// ...
// Dans Promise.all:
if (resultStr.startsWith('Tool error:')) {
  const errKey = resultStr.slice(0, 200);
  if (errKey === lastErrorMsg) {
    consecutiveErrors++;
  } else {
    consecutiveErrors = 1;
    lastErrorMsg = errKey;
  }
} else {
  consecutiveErrors = 0;
  lastErrorMsg = '';
}

Les variables consecutiveErrors et lastErrorMsg sont modifiees dans les callbacks de Promise.all. Si 3 tools executent en parallele et les 3 echouent avec la meme erreur, les 3 callbacks incrementent consecutiveErrors de maniere potentiellement non-deterministe. En JavaScript single-thread, chaque callback s'execute atomiquement entre les await, donc l'increment final sera correct (3). Cependant :

  1. Si 2 tools echouent et 1 reussit, l'ordre de resolution determine si consecutiveErrors est remis a 0 (par le succes) puis incremente, ou incremente puis remis a 0. Le resultat depend de l'ordre d'execution.
  2. Cela peut causer des faux positifs (auto-stop) ou faux negatifs (pas d'auto-stop malgre des erreurs repetees).

Classification : BUG PROBABLE — la semantique de "consecutive" ne s'applique pas proprement aux executions paralleles. L'impact est faible (l'auto-stop a 3 erreurs est un garde-fou secondaire).


10.12 pipelines/index.ts — Pipeline bien structure, pas de bug

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/pipelines/index.ts Severite : AUCUNE

Le module est correctement implemente :

Seul point d'attention : les pipelines predefinis (create_frontend, create_fullstack, etc.) supposent que les agents gitlab-dev, qwik-dev, elysia-dev, pg-dev existent dans le registre. C'est le cas actuellement mais si un agent est renomme, le pipeline echouera proprement (message d'erreur "Agent not found").

Classification : PAS DE BUG


10.13 db/index.ts — searchConversations SQL injection potentielle

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/db/index.ts lignes 516-523

async searchConversations(userId: string, query: string, limit = 20): Promise<DBConversation[]> {
  const titleResults = await this.client.dbQuery('agents.conversations', {
    filter: { user_id: userId, title: `ilike.*${query}*` },
    // ...
  });
  const msgResults = await this.client.dbQuery('agents.chat_messages', {
    select: 'conversation_id',
    filter: { user_id: userId, content: `ilike.*${query}*` },
    // ...
  });

Le query utilisateur est injecte directement dans le filtre PostgREST ilike.*${query}*. PostgREST echappe les valeurs dans les filtres (c'est un parametre URL, pas du SQL brut), donc il n'y a pas d'injection SQL directe. Cependant :

Verification dans buildFilterQuery (client.ts lignes 471-484) : la methode verifie si la valeur commence par eq., gt., ilike., etc., et si oui, la passe telle quelle. Ici, la valeur est deja prefixee par ilike.*, donc elle sera detectee comme un operateur PostgREST et passee telle quelle. Le query de l'utilisateur est concatene a l'interieur du pattern ilike.*${query}*. Si query contient des * ou des %, cela modifie le pattern de recherche mais ne cause pas d'injection SQL.

Classification : RISQUE THEORIQUE — pas d'injection SQL, mais le pattern de recherche peut etre altere par des caracteres speciaux dans l'input utilisateur.


10.14 db/index.ts — forkConversation charge 10000 messages en memoire

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/db/index.ts ligne 590

async forkConversation(originalConvId: string, userId: string, upToMessageId: string, newTitle: string): Promise<...> {
  const allMessages = await this.getChatMessages(originalConvId, 10000);

La fonction charge jusqu'a 10000 messages en memoire pour trouver le upToMessageId. Chaque message est ensuite copie un par un avec des saveChatMessage sequentiels (ligne 600-610). Pour une conversation de 1000 messages, c'est 1 query + 1000 inserts sequentiels via l'API REST → potentiellement >30 secondes de latence.

Classification : PROBLEME ARCHITECTURAL — fonctionne mais tres lent pour les longues conversations. Une solution serait de copier les messages cote serveur via une RPC PostgreSQL.


10.15 db/index.ts — saveMessages insert sequentiel sans batch

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/db/index.ts lignes 262-274

async saveMessages(messages: DBMessage[]): Promise<void> {
  for (const msg of messages) {
    await this.client.dbInsert('agents.messages', { ... });
  }
}

Chaque message est insere un par un via des appels REST sequentiels. PostgREST supporte les inserts batch (envoyer un tableau d'objets en POST), ce qui serait bien plus performant. Ce n'est pas un bug mais une opportunite d'optimisation.

Classification : QUALITE DE CODE — pas de bug, optimisation possible.


10.16 .env.deploy — GITLAB_TOKEN en clair, duplique avec conf.prod.gouroubleu.yml

Fichier : /stock_8to/33800-stack/projects/ulias-org/.env.deploy

PORT=5515
NODE_ENV=production
CONNECTORS_API_URL=http://192.168.1.12:5403
GITLAB_URL=https://gitlab.33800.nowhere84.com
GITLAB_TOKEN=glpat-yaowLwWBJhXfzJEC8UBC

Ce fichier est auto-genere par smart-deploy.sh (voir le commentaire en tete). Il contient le meme GITLAB_TOKEN que conf.prod.gouroubleu.yml. Le fichier est versionne dans le repo GitLab (verifie : pas de .gitignore qui l'exclut).

Probleme supplementaire : CONNECTORS_API_URL utilise l'IP 192.168.1.12:5403 au lieu du domaine HTTPS. C'est un fichier de config Docker deploy (pas du code applicatif), donc la regle Pi-hole ne s'applique pas strictement — les containers Docker communiquent via les IPs internes car ils n'utilisent pas Pi-hole comme DNS. Cependant, c'est un risque de maintenance si l'IP change.

Classification : RISQUE THEORIQUE (memes remarques que finding 3.2 — token en clair dans un repo prive).


10.17 Dockerfile — Pas de multi-stage build, image incluant les sources

Fichier : /stock_8to/33800-stack/projects/ulias-org/Dockerfile

FROM oven/bun:1 AS base
WORKDIR /app
COPY package.json bun.lockb* ./
COPY packages/server/package.json ./packages/server/
RUN bun install --frozen-lockfile 2>/dev/null || bun install
COPY tsconfig.json ./
COPY packages/server/ ./packages/server/
EXPOSE 5515
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD curl -f http://localhost:5515/health || exit 1
CMD ["bun", "run", "packages/server/src/index.ts"]

Le Dockerfile est fonctionnel et bien structure (cache des deps avant le code, healthcheck). Points d'attention :

  1. Pas de multi-stage : Bun execute TypeScript directement, donc un build step n'est pas strictement necessaire. L'image inclut les sources TS, ce qui est le pattern standard Bun.
  2. 2>/dev/null || bun install : Si le lockfile est corrompu, --frozen-lockfile echoue silencieusement et bun install recree le lockfile, potentiellement avec des versions differentes de la prod precedente. C'est un risque de reproductibilite.
  3. healthcheck avec curl : L'image oven/bun:1 est basee sur Debian et inclut curl. Le check est correct.

Classification : QUALITE DE CODE — fonctionnel, le fallback bun install est un risque mineur de reproductibilite.


10.18 conf.prod.gouroubleu.yml — nginx non marque 'private'

Fichier : /stock_8to/33800-stack/projects/ulias-org/conf.prod.gouroubleu.yml lignes 14-18

nginx:
  enabled: true
  domain: "ulias-org.33800.nowhere84.com"
  ssl: true
  websocket: true

La section nginx ne contient PAS private: true. Cela signifie que le service est accessible publiquement sur Internet via le domaine ulias-org.33800.nowhere84.com.

Combinee avec le finding 3.1 (GET /api/agents sans auth expose les prompts) et le finding 6.1 (endpoints /internal/* sans auth), cela implique que :

La combinaison des findings 3.1 + 10.18 eleve la severite : ce n'est plus "theorique" si le service est public. Un scan DNS ou un brute-force de sous-domaines peut reveler ulias-org.33800.nowhere84.com.

Classification : RISQUE DE SECURITE — le service devrait etre private: true ou les endpoints sensibles devraient etre proteges par auth.


10.19 agent-runner/index.ts — response cache global partage entre sessions

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/agent-runner/index.ts ligne 14

const responseCache = new ResponseCache(50, 5 * 60 * 1000);

Le cache de reponses est global (meme variable pour toutes les sessions, tous les agents). Si deux sessions differentes envoient le meme message au meme agent, la deuxieme recevra la reponse cachee de la premiere. En mono-utilisateur, c'est un gain de performance. En multi-tenant, c'est une fuite d'information.

Le hash de cache (response-cache.ts) utilise model + systemPrompt[:200] + lastUserMessage. Deux sessions avec le meme agent et le meme message obtiendront le meme hash.

Classification : RISQUE THEORIQUE — en mono-utilisateur, c'est un feature. En multi-tenant, c'est critique.


10.20 auth.ts — userId construit de maniere fragile

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/auth.ts ligne 28

const userId = data.user_id || data.userId || apiKey.slice(0, 8);

Si connectors-api ne renvoie ni user_id ni userId dans la reponse /api/user/instances, le userId est construit a partir des 8 premiers caracteres de l'API key. C'est un fallback fragile :

  1. Deux API keys commencant par les memes 8 caracteres auraient le meme userId
  2. Le userId change si l'API key change (les conversations, objectifs, etc. deviennent inaccessibles)
  3. Les 8 premiers chars de l'API key sont souvent un prefixe commun (ex: toutes les keys commencent par "ula_")

Impact : En pratique, connectors-api renvoie toujours un user_id. Le fallback n'est utilise que si l'API est en mode degrade.

Classification : BUG PROBABLE — le fallback est fragile et peut causer des collisions d'identite.


10.21 index.ts — Endpoint /api/upload decode base64 en UTF-8 (perte de donnees binaires)

Fichier : /stock_8to/33800-stack/projects/ulias-org/packages/server/src/index.ts lignes 891-893

const decoded = Buffer.from(content, 'base64').toString('utf-8');
await session.toolsClient.storageUpload(bucket, path, decoded, contentType || 'application/octet-stream');

Le contenu base64 est decode en string UTF-8 avant d'etre envoye au storage. Si le fichier est binaire (image, PDF, etc.), la conversion en UTF-8 corrompt les donnees. Le contentType par defaut est application/octet-stream (binaire), mais le contenu est traite comme texte.

Classification : BUG CERTAIN — les uploads binaires sont corrompus. Seuls les fichiers texte (HTML, CSS, JS, JSON) fonctionnent correctement.


11. SYNTHESE COMPLETE MISE A JOUR

# Finding Classification Severite Fichier
1.1 Briefing processAnswer logique inversee BUG CERTAIN HAUTE briefing/index.ts
1.2 CLI port 5510 au lieu de 5515 BUG CERTAIN MOYENNE cli/bin/ulias.ts
1.3 ws.send apres deconnexion BUG CERTAIN MOYENNE index.ts
10.3 git-workflow '??' teste deux fois, untracked jamais assigne BUG CERTAIN BASSE tools/git-workflow.ts
10.21 Upload base64 decode en UTF-8 corrompt les binaires BUG CERTAIN MOYENNE index.ts
2.1 CALLBACK_BASE_URL hardcode sans env BUG PROBABLE HAUTE tools/client.ts
2.2 CONNECTORS_API_URL fallback 5400 BUG PROBABLE BASSE index.ts
2.3 Cache hash faible BUG PROBABLE BASSE tools/response-cache.ts
2.4 message_count non-atomique BUG PROBABLE BASSE index.ts
2.5 NotificationService inutilisee BUG PROBABLE BASSE notifications/index.ts
10.7 storageGetPublicUrl IP hardcodee BUG PROBABLE BASSE tools/client.ts
10.10 compactToolResult num_ctx ignore (meme pb que 7.1) BUG PROBABLE BASSE agent-runner/index.ts
10.11 consecutiveErrors semantique incorrecte en parallele BUG PROBABLE BASSE agent-runner/index.ts
10.20 userId fallback fragile (apiKey[:8]) BUG PROBABLE BASSE auth.ts
3.1 /api/agents sans auth RISQUE THEORIQUE BASSE index.ts
3.2 GITLAB_TOKEN en clair RISQUE THEORIQUE MOYENNE conf.prod.gouroubleu.yml
3.3 Injection heredoc writeFile RISQUE THEORIQUE BASSE tools/client.ts
3.4 Pas de limite taille WS RISQUE THEORIQUE BASSE index.ts
10.1 Estimation token approximative RISQUE THEORIQUE BASSE context-builder/index.ts
10.5 Circuit breaker global partage entre agents RISQUE THEORIQUE BASSE agent-runner/index.ts
10.13 searchConversations pattern alterable RISQUE THEORIQUE BASSE db/index.ts
10.16 .env.deploy GITLAB_TOKEN en clair RISQUE THEORIQUE BASSE .env.deploy
10.18 nginx non private + endpoints sans auth exposés RISQUE SECURITE HAUTE conf.prod.gouroubleu.yml
10.19 Response cache global partage entre sessions RISQUE THEORIQUE BASSE agent-runner/index.ts
4.1 HTTP status codes FAUX POSITIF
4.2 Port 5400 en prod FAUX POSITIF
10.9 toolCallsCount non-thread-safe dans Promise.all FAUX POSITIF agent-runner/index.ts
10.4 git-workflow code mort CODE MORT BASSE tools/git-workflow.ts
10.6 TOOL_SETS['storage-dev'] jamais utilise CODE MORT BASSE tools/definitions.ts
10.12 Pipelines bien structures PAS DE BUG pipelines/index.ts

12. RECOMMANDATIONS MISES A JOUR (par ordre de priorite)

  1. SECURITE : Ajouter private: true dans nginx (10.18) — Le service est public, les prompts et endpoints internes sont exposes
  2. Corriger le bug briefing (1.1) — Impact direct sur l'UX du workflow de creation de projet
  3. Corriger l'upload binaire (10.21) — Les uploads non-texte sont corrompus
  4. Ajouter CALLBACK_BASE_URL dans conf.prod.gouroubleu.yml (2.1) — Rendre explicite ce qui est implicite
  5. Corriger le port CLI (1.2) — De 5510 a 5515
  6. Ajouter un guard ws.send (1.3) — Verifier ws.readyState === 1 avant chaque send
  7. Corriger git-workflow '??' logic (10.3) — Inverser l'ordre des conditions
  8. Deplacer GITLAB_TOKEN vers un secret Docker (3.2 + 10.16) — Pas de token en clair dans le repo
  9. Ajouter auth sur /api/agents (3.1) — Verification triviale a ajouter
  10. Corriger storageGetPublicUrl IP (10.7) — Utiliser le domaine HTTPS
  11. Supporter num_ctx dans LLMOptions (7.1 + 10.10) — Ou documenter que c'est toujours 16384
  12. Optimiser la traduction (5.3) — Detecter la langue avant de traduire
  13. Nettoyer le code mort (10.4, 10.6, 2.5) — git-workflow.ts, TOOL_SETS['storage-dev'], NotificationService

Audit initial realise par lecture integrale de 16 fichiers TypeScript, 1 YAML, 1 Dockerfile, 2 package.json. Complement realise le 15-02-2026 par lecture integrale des 6 fichiers non couverts + re-verification croisee de tous les fichiers. Total : 16 fichiers .ts, 1 YAML, 1 .env.deploy, 1 Dockerfile. Chaque finding trace jusqu'au code source exact avec verification des chemins d'execution.