Pourquoi Cradle n'est plus une application de bureau : ce qu'Electron coûte à un développeur seul
Open Cradle a commencé comme une application de bureau sur Electron. Tout le moteur vivait dedans : node-llama-cpp pour l'inférence, better-sqlite3 et sqlite-vec pour le stockage et la recherche vectorielle, un triage opérateur avec une évaluation des risques à deux couches. Le raisonnement était direct : les données ne quittent pas la machine de l'opérateur, puisque l'application est cette machine.
Ce n'est plus le cas. Voici pourquoi j'ai abandonné Electron, ce qu'il coûtait, et ce qui l'a remplacé.
Ce qui a changé dans le cahier des charges
La première version répondait à la question « comment donner un assistant local à un opérateur ». La version actuelle répond à une autre question : comment placer une control layer entre les agents, les données, les outils et les humains. La formule autour de laquelle le produit est construit :
LLM proposes → Evidence proves → Ontology constrains → Policy permits → Cradle executes + records.
Je ne construis pas mon propre multi-agent framework. Les agents sont faits par des outils externes ; Cradle fixe les limites : ce qu'un agent peut proposer, ce qui est permis parmi ces propositions, ce qui a réellement été exécuté et enregistré.
Une précision honnête sur les ontologies. C'est une direction d'architecture, pas une fonctionnalité qui marche. Le moteur de vérification existe ; le registre des vérificateurs est encore vide. Si vous lisez quelque part que Cradle « contraint les actions par une ontologie », cela décrit l'objectif, pas l'état actuel.
Une fois le cahier des charges changé, le bureau n'en découlait plus. Une control layer est un serveur, pas une fenêtre sur un portable.
Ce qu'Electron coûtait
Tant que le produit était une application de bureau, le surcoût ressemblait au prix de la localité. Pour un développeur seul, il s'additionnait ainsi :
- Signature et notarization macOS. Chaque release, 3 à 10 minutes d'attente du notary service, et de temps en temps le processus restait simplement bloqué.
- Le runner macOS de GitHub Actions est facturé avec un multiplicateur de 10× sur les minutes. Le build mac se faisait donc en local, et l'installeur Windows NSIS dans la CI.
- Les modules natifs recompilés entre deux ABI. Les tests tournent sous Node, l'application sous Electron, et les deux n'ont pas le même ABI. better-sqlite3 et node-llama-cpp étaient recompilés dans un sens puis dans l'autre ; le script de test commence encore par
pnpm rebuild better-sqlite3. - sqlite-vec livre des binaires par plateforme, un par OS et par architecture.
- Pas de hot reload dans le processus main. Tout changement dans l'IPC ou le moteur imposait de relancer l'application.
- Trois plateformes = trois chaînes de release, chacune avec sa signature, son installeur et ses propres façons de casser.
Aucun de ces points n'est fatal seul. Ensemble, ils mangeaient un temps qu'une personne seule, au début d'un produit, n'a pas.
Ce qui l'a remplacé
cradle-server est un processus headless. Il tourne en local ou sur un serveur GPU. L'UI est une console web : le même code d'interface qui vivait dans le renderer Electron, posé sur un HttpBackend au lieu du pont IPC.
Le geste clé est un registre de handlers RPC. Le corps d'un handler s'écrit une fois. Sur le bureau, bindRpcToIpc le relie à l'IPC ; sur le serveur, la route POST /api/v1/rpc/:channel le sert. Les événements qui passaient par webContents.send circulent maintenant en SSE.
// One handler body, two transports.
registerRpc('agents:list', {
handler: (_args, ctx) => listAgents(ctx.projectId),
scopes: ['agents:read']
})
// Desktop: ipcMain.handle('agents:list', ...) — via bindRpcToIpc('agents')
// Server: POST /api/v1/rpc/agents:list — via registerRpcRoutes(server)
// Events: webContents.send(...) — now SSE on GET /api/v1/events
La route vérifie d'abord les scopes de la clé et ne révèle qu'ensuite si le canal existe : un appel anonyme reçoit toujours un 401, jamais un indice sur la forme de l'API.
La clé API d'un point n'est stockée que dans le navigateur. La console la garde dans le localStorage ; le compte ne conserve que le nom et l'URL du point. Le serveur qui héberge la console ne voit jamais les clés de vos instances cradle-server.
Mode entièrement local
pnpm console:local
Une seule commande démarre le site avec la console sur localhost:3000 et cradle-server sur :31416. Sans Firebase, sans e-mail, sans service externe : le code de connexion s'affiche dans le terminal, la clé API admin dans le log du serveur au premier démarrage. C'est le mode « tout sur une machine » par lequel l'application de bureau avait commencé, moins Electron.
Trois types de points
Les réglages de la console permettent de basculer entre les points auxquels elle se connecte :
- Machine locale — cradle-server sur le portable, modèles GGUF.
- Votre propre serveur GPU — le même cradle-server, déplacé sur du matériel à l'intérieur du périmètre.
- Un worker EC2 de test avec des modèles Bedrock — réservé aux expérimentations. Une instance GPU sur EC2 s'est révélée trop chère, et Bedrock permet de dérouler des scénarios sans matériel à soi. Pour un circuit fermé, cette option est exclue par définition.
Limites
- Electron n'est pas encore retiré du dépôt. Forge, le preload et les liaisons IPC cohabitent avec le serveur jusqu'à ce que la console atteigne la parité fonctionnelle.
- Un site servi en https ne peut pas appeler
http://<ip>: le navigateur bloque le mixed content. Les navigateurs tolèrenthttp://localhost, mais un point distant a besoin de TLS. - Les ontologies : voir plus haut. Une direction, pas une fonctionnalité.
Sources
- Electron, Code Signing — exigences de signature et de notarization sur macOS.
- Electron, Native Node Modules — pourquoi les modules natifs doivent être recompilés pour l'ABI d'Electron.
- GitHub Docs, About billing for GitHub Actions — multiplicateurs de minutes pour les runners Windows et macOS.