33800 Docs

← Retour

Proposition : Ulias Org v5 — Connectors-Powered + Multi-User

Date : 12/02/2026 15:00 Priorite : HAUTE Status : EN ATTENTE DE VALIDATION Base : v4 (separation projet + CLI live)


Changements v4 → v5

  1. Connectors-api = couche d'acces universelle — ulias-org n'a AUCUN credential en dur, AUCUN acces direct. Tout passe par connectors-api.
  2. Multi-utilisateur — N'importe qui avec un compte connector peut utiliser l'org. Chaque user a ses propres instances, permissions, profil CEO.
  3. Wrapper tools — Un layer d'abstraction dans ulias-org qui expose des fonctions propres aux agents et appelle connectors-api en interne.

Tout le reste (equipes, agents, CLI, Web UI, Context Builder, apprentissage) reste inchange.


1. Architecture Connectors-Powered

Principe

Aujourd'hui, connectors-api gere deja l'authentification, les instances, et le proxy vers tous les services. Plutot que de recabler tout ca dans ulias-org, on reutilise cette brique a 100%.

┌─────────────────────────────────────────────────────────────────┐
│                        ULIAS-ORG                                │
│                                                                  │
│  ┌──────┐ ┌──────────┐ ┌────────────┐ ┌───────────────────┐    │
│  │ CLI  │ │  Web UI  │ │  Director  │ │  26 micro-agents  │    │
│  └──┬───┘ └────┬─────┘ └─────┬──────┘ └────────┬──────────┘    │
│     └──────────┴──────┬──────┴───────────────────┘               │
│                       │                                          │
│              ┌────────┴────────┐                                 │
│              │  TOOLS LAYER    │  ← Fonctions propres :          │
│              │                 │    exec(), readFile(),           │
│              │  Chaque outil = │    queryLoki(), gitPush(),       │
│              │  un wrapper qui │    createJob(), dockerPs()...    │
│              │  appelle        │                                  │
│              │  connectors-api │                                  │
│              └────────┬────────┘                                 │
│                       │                                          │
│              ┌────────┴────────┐                                 │
│              │  USER CONTEXT   │  ← API key connector du user    │
│              │                 │    → determine les instances     │
│              │  api_key: "..." │    accessibles et les perms     │
│              └────────┬────────┘                                 │
└───────────────────────┼──────────────────────────────────────────┘
                        │
                        │ POST /api/fetch (avec X-API-Key du user)
                        │
               ┌────────▼────────┐
               │ CONNECTORS-API  │
               │                 │
               │ Route vers :    │
               │ - SSH instances │
               │ - ai-orchestr.  │
               │ - Supabase      │
               │ - GitLab        │
               │ - Loki/Grafana  │
               │ - ntfy/Mailjet  │
               │ - Proxmox       │
               │ - Nextcloud     │
               │ - Jellyfin      │
               │ - etc.          │
               └─────────────────┘

Zero credential dans ulias-org

Avant (v4) Apres (v5)
SUPABASE_SERVICE_KEY en env var Via connector instance Supabase du user
GITLAB_TOKEN en env var Via connector instance GitLab du user
SSH keys montees en volume Via connector instance SSH du user
AI_ORCHESTRATOR_URL en dur Via connector instance ai-orchestrator du user
NTFY_URL en env var Via connector instance ntfy du user

La seule config d'ulias-org = l'URL de connectors-api. C'est tout.

Onboarding d'un nouvel utilisateur

  1. L'utilisateur a deja un compte sur connectors-api (API key)
  2. Il a configure ses instances : SSH machines, GitLab, Supabase, ai-orchestrator...
  3. Il lance ulias --api-key <sa-cle> ou se connecte sur la Web UI
  4. ulias-org detecte automatiquement les instances disponibles :
    • "Tu as 3 instances SSH, 1 GitLab, 1 ai-orchestrator, 1 Supabase"
    • "L'org va utiliser ai-orchestrator pour l'inference, SSH pour les machines..."
  5. C'est pret. L'org travaille avec les ressources du user.
$ ulias --api-key cxn_abc123

  ╭─────────────────────────────────────────────╮
  │  Ulias Org v0.1.0                           │
  │  User: gouroubleu                           │
  │  Instances detectees:                       │
  │    SSH: PVE, prod-portainer, nginx, win11   │
  │    LLM: ai-orchestrator (2 GPUs)            │
  │    DB:  Supabase                            │
  │    Git: GitLab                              │
  │    Notif: ntfy, Mailjet                     │
  │  7 equipes · 26 agents · ready              │
  ╰─────────────────────────────────────────────╯

CEO >

2. Tools Layer — L'abstraction pour les agents

Chaque agent recoit des fonctions typees qui cachent la complexite de connectors-api. L'agent ne sait pas qu'il passe par un proxy — il appelle tools.exec() et ca marche.

Catalogue des tools

// === EXECUTION ===

// Executer une commande sur une machine
tools.exec(instance: string, command: string): Promise<ExecResult>
// → POST /api/fetch { connector: "ssh", instance, endpoint: "exec", body: { command } }

// Lire un fichier distant
tools.readFile(instance: string, path: string): Promise<string>
// → tools.exec(instance, `cat ${path}`)

// Ecrire un fichier distant
tools.writeFile(instance: string, path: string, content: string): Promise<void>
// → tools.exec(instance, `cat > ${path} << 'ULIAS_EOF'\n${content}\nULIAS_EOF`)

// Editer un fichier (sed-like, avec lecture prealable)
tools.editFile(instance: string, path: string, oldStr: string, newStr: string): Promise<void>

// Lister fichiers
tools.glob(instance: string, pattern: string): Promise<string[]>

// Rechercher dans fichiers
tools.grep(instance: string, pattern: string, path: string): Promise<GrepResult[]>


// === GIT ===

tools.gitClone(instance: string, repo: string, dir: string): Promise<void>
tools.gitDiff(instance: string, dir: string): Promise<string>
tools.gitCommit(instance: string, dir: string, message: string, files: string[]): Promise<string>
tools.gitPush(instance: string, dir: string): Promise<void>
tools.gitLog(instance: string, dir: string, limit?: number): Promise<GitLogEntry[]>


// === DOCKER ===

tools.dockerPs(instance: string): Promise<Container[]>
tools.dockerLogs(instance: string, container: string, lines?: number): Promise<string>
tools.dockerRestart(instance: string, container: string): Promise<void>
tools.dockerInspect(instance: string, container: string): Promise<object>


// === LLM (via ai-orchestrator) ===

tools.llmChat(messages: Message[], options?: LLMOptions): Promise<LLMResponse>
// → POST /api/fetch { connector: "ai-orchestrator", instance, endpoint: "jobs",
//     body: { tool_id: "ollama", job_type: "chat", input_params: { model, messages, tools } } }
// → Poll job status until complete

tools.llmEmbed(text: string): Promise<number[]>
// → Meme chose avec job_type: "embeddings"


// === DATABASE (Supabase) ===

tools.dbQuery(table: string, query: QueryOptions): Promise<any[]>
// → POST /api/fetch { connector: "supabase", instance, endpoint: "rest",
//     body: { table, select, filter, order, limit } }

tools.dbInsert(table: string, data: object): Promise<any>
tools.dbUpdate(table: string, id: string, data: object): Promise<any>
tools.dbDelete(table: string, id: string): Promise<void>


// === LOGS (Loki) ===

tools.queryLogs(query: string, start?: Date, end?: Date, limit?: number): Promise<LogEntry[]>
// → POST /api/fetch { connector: "loki" | "grafana", ... }


// === NOTIFICATIONS ===

tools.notify(title: string, message: string, priority?: string): Promise<void>
// → POST /api/fetch { connector: "ntfy", ... }

tools.sendMail(to: string, subject: string, body: string): Promise<void>
// → POST /api/fetch { connector: "mailjet", ... }


// === HTTP GENERIQUE ===

tools.httpGet(instance: string, path: string): Promise<any>
tools.httpPost(instance: string, path: string, body: object): Promise<any>

Implementation du wrapper

// src/tools/base.ts
export class ToolsClient {
  constructor(
    private connectorsUrl: string,
    private apiKey: string,
    private userInstances: Map<string, ConnectorInstance>
  ) {}

  // Appel generique a connectors-api
  private async fetch(connector: string, instance: string, endpoint: string, options: {
    method?: string,
    body?: any,
    query?: Record<string, string>
  } = {}): Promise<any> {
    const res = await fetch(`${this.connectorsUrl}/api/fetch`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': this.apiKey
      },
      body: JSON.stringify({
        connector_type: connector,
        instance_id: instance,
        endpoint,
        method: options.method || 'GET',
        body: options.body,
        query: options.query
      })
    });
    return res.json();
  }

  // === Tool implementations ===

  async exec(instance: string, command: string): Promise<ExecResult> {
    return this.fetch('ssh', instance, 'exec', {
      method: 'POST',
      body: { command }
    });
  }

  async readFile(instance: string, path: string): Promise<string> {
    const result = await this.exec(instance, `cat "${path}"`);
    return result.stdout;
  }

  async llmChat(messages: Message[], options: LLMOptions = {}): Promise<LLMResponse> {
    // Trouver l'instance ai-orchestrator du user
    const aiInstance = this.findInstance('ai-orchestrator');

    // Creer le job
    const job = await this.fetch('ai-orchestrator', aiInstance, 'jobs', {
      method: 'POST',
      body: {
        tool_id: options.gpu === 1 ? 'ollama-gpu1' : 'ollama',
        job_type: 'chat',
        priority: options.priority || 0,
        input_params: {
          model: options.model || 'devstral-small-2',
          messages,
          tools: options.tools,
          temperature: options.temperature || 0.1
        }
      }
    });

    // Poll jusqu'a completion
    return this.pollJob(aiInstance, job.id);
  }

  // ... etc
}

Mapping instance auto

Au login, ulias-org liste les instances du user via GET /api/instances et les categorise :

// Auto-detection des instances disponibles
const instances = await connectorsApi.listInstances(apiKey);

const mapping = {
  ssh: instances.filter(i => i.connector_type === 'ssh'),
  aiOrchestrator: instances.find(i => i.connector_type === 'ai-orchestrator'),
  supabase: instances.find(i => i.connector_type === 'supabase'),
  gitlab: instances.find(i => i.connector_type === 'gitlab'),
  loki: instances.find(i => i.connector_type === 'loki' || i.connector_type === 'grafana'),
  ntfy: instances.find(i => i.connector_type === 'ntfy'),
  mailjet: instances.find(i => i.connector_type === 'mailjet'),
  proxmox: instances.find(i => i.connector_type === 'proxmox'),
};

// Verifier les prerequis minimaux
if (!mapping.aiOrchestrator) throw new Error('Instance ai-orchestrator requise');
if (!mapping.supabase) throw new Error('Instance Supabase requise');
if (mapping.ssh.length === 0) console.warn('Aucune instance SSH — agents Ops limites');

3. Multi-User

Schema DB adapte

Toutes les tables du schema agents recoivent un user_id :

-- Chaque user a son propre espace dans l'org
ALTER TABLE agents.objectives ADD COLUMN user_id TEXT NOT NULL;
ALTER TABLE agents.tasks ADD COLUMN user_id TEXT NOT NULL;
ALTER TABLE agents.decisions ADD COLUMN user_id TEXT NOT NULL;
ALTER TABLE agents.metrics ADD COLUMN user_id TEXT NOT NULL;
ALTER TABLE agents.lessons ADD COLUMN user_id TEXT NOT NULL;
ALTER TABLE agents.ceo_profile ADD COLUMN user_id TEXT NOT NULL;
ALTER TABLE agents.ceo_interactions ADD COLUMN user_id TEXT NOT NULL;
ALTER TABLE agents.prompt_versions ADD COLUMN user_id TEXT NOT NULL;
ALTER TABLE agents.routing_rules ADD COLUMN user_id TEXT NOT NULL;

-- Index pour les requetes par user
CREATE INDEX idx_objectives_user ON agents.objectives(user_id);
CREATE INDEX idx_tasks_user ON agents.tasks(user_id);
-- etc.

Isolation

Aspect Isolation
Objectifs, taches, decisions Par user (chacun voit les siens)
Profil CEO Par user (chaque user a son profil)
Lecons apprises Par user + pool global (lecons generiques partagees)
Prompts agents Globaux par defaut, customisables par user
Routing modeles Global (meme modeles pour tous)
GPU Partage via ai-orchestrator (queue avec priorites)
Instances SSH/Git Par user (chacun a ses propres machines)

Auth

L'authentification est delegue a connectors-api. Ulias-org ne gere pas de comptes :

User envoie API key connector → ulias-org la forward a connectors-api
→ connectors-api valide la cle, retourne user_id + instances
→ ulias-org stocke le user_id dans la session
→ Tous les appels tools sont faits avec l'API key du user

Pas de table users dans ulias-org. Pas de signup. Pas de login custom. Le user existe dans connectors-api → il peut utiliser ulias-org. Point.


4. Consequences sur le conf.prod.gouroubleu.yml

name: "ulias-org"
target: "prod-portainer"
type: "bun"
domain: "ulias-org.33800.nowhere84.com"
port: 5510

env:
  # LA SEULE CONFIG NECESSAIRE
  CONNECTORS_API_URL: "http://192.168.1.12:5400"

health: "/health"

nginx:
  enabled: true
  ssl: true
  websocket: true

logs:
  enabled: true
  driver: loki

C'est tout. Plus de SUPABASE_URL, GITLAB_TOKEN, SSH keys, CLAUDE_API_KEY. Tout passe par connectors-api via l'API key du user.


5. Connector instances necessaires (pour ton compte)

Pour que l'org fonctionne completement avec ton compte connector, ces instances doivent exister :

Instance Connector Type Usage Org Existe ?
ai-orchestrator ai-orchestrator (custom) LLM inference A creer
Supabase supabase DB agents.* Existe
PVE-SSH ssh Acces hyperviseur Existe
prod-portainer-SSH ssh Docker, deploiement Existe
nginx-SSH ssh Reverse proxy Existe
win11-SSH ssh GPUs, outils IA Existe
gitlab-SSH ssh GitLab server Existe
GitLab API gitlab (custom) Git operations A verifier
Loki/Grafana grafana (custom) Query logs A verifier
ntfy ntfy (custom) Notifications push A verifier
Mailjet mailjet Notifications mail Existe

Les instances "A creer" seront creees dans le Sprint 1.


6. Impact sur le Tools Layer des agents

Chaque agent dans sa definition de prompt recoit les tools sous forme de function definitions :

// Prompt systeme de l'agent Coder
const coderTools = [
  {
    name: 'exec',
    description: 'Execute a shell command on a remote machine',
    parameters: {
      instance: { type: 'string', description: 'SSH instance name (e.g. prod-portainer-SSH)' },
      command: { type: 'string', description: 'Shell command to run' }
    }
  },
  {
    name: 'readFile',
    description: 'Read a file from a remote machine',
    parameters: {
      instance: { type: 'string' },
      path: { type: 'string' }
    }
  },
  {
    name: 'editFile',
    description: 'Edit a file: replace old_string with new_string',
    parameters: {
      instance: { type: 'string' },
      path: { type: 'string' },
      old_string: { type: 'string' },
      new_string: { type: 'string' }
    }
  },
  {
    name: 'gitCommit',
    description: 'Stage files and commit',
    parameters: {
      instance: { type: 'string' },
      dir: { type: 'string' },
      message: { type: 'string' },
      files: { type: 'array', items: { type: 'string' } }
    }
  },
  {
    name: 'gitPush',
    description: 'Push commits to remote',
    parameters: {
      instance: { type: 'string' },
      dir: { type: 'string' }
    }
  }
];

// L'agent appelle tools par nom → le ToolsClient execute via connectors-api
// Le modele LLM genere : { tool: "readFile", args: { instance: "prod-portainer-SSH", path: "/app/src/index.ts" } }
// → ToolsClient.readFile("prod-portainer-SSH", "/app/src/index.ts")
// → POST connectors-api /api/fetch { connector: "ssh", instance: "prod-portainer-SSH", ... }

Avantage cle

L'agent ne sait pas :

Il sait juste : "prod-portainer-SSH" est une machine ou je peux executer des commandes. Le contexte de l'agent reste minimal et pur — exactement la philosophie micro-agent.


7. Plan de Construction (mis a jour)

Sprint 0 — Validation modeles (2-3 jours)

Inchange.

Sprint 1 — Fondations + CLI + Connector Integration (1 semaine)

Sprints 2-8

Inchanges (les agents utilisent le Tools Layer, transparent pour eux).


8. Resume des changements v4 → v5

Aspect v4 v5
Credentials Env vars (SSH keys, tokens, URLs) Zero — tout via connectors-api
Acces services Direct (HTTP, SSH) Via connectors-api proxy
Auth users Implicite (mono-user) API key connector (multi-user)
Config container 6+ env vars 1 seule (CONNECTORS_API_URL)
Tools des agents Appels directs Wrapper layer typé
Onboarding Configurer SSH, tokens, etc. "Donne ton API key connector"
Schema DB Sans user_id Avec user_id partout

L'avantage killer : n'importe qui qui a deja configure ses connecteurs (SSH, GitLab, etc.) peut utiliser l'org en 30 secondes. Zero setup supplementaire.