Date : 12/02/2026 15:00 Priorite : HAUTE Status : EN ATTENTE DE VALIDATION Base : v4 (separation projet + CLI live)
Tout le reste (equipes, agents, CLI, Web UI, Context Builder, apprentissage) reste inchange.
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. │
└─────────────────┘
| 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.
ulias --api-key <sa-cle> ou se connecte sur la Web UI$ 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 >
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.
// === 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>
// 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
}
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');
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.
| 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) |
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.
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.
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.
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", ... }
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.
Inchange.
gouroubleu/ulias-orgagents avec user_id)ulias --api-key ..., chat, spinners, /statusInchanges (les agents utilisent le Tools Layer, transparent pour eux).
| 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.