mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 23:18:13 +02:00
Rebaptise AJEAN en Loki (fork, lignée conservée)
- module github.com/R0m1k3/Loki, cmd/loki, internal/loki (package loki) - LOKI_HOME, LOKI_MODEL_DIRS, LOKI_SERVICE, LOKI_DL_CONNS ; /etc/loki ; units loki-engine / loki-ui ; binaire et aide CLI - updateRepo pointe sur R0m1k3/Loki (l'auto-update ne tirera plus les binaires AJEAN amont) Conservé à l'identique : le domaine ajean.link (service de tunnel amont), les littéraux de migration 0.7.x (migrate_07.go), RELEASE_NOTES.md et LICENSE (historique et licence de l'amont). go build/vet/test : verts.
This commit is contained in:
157 files changed
+928
-928
No files matched your search
@@ -0,0 +1,258 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Configuration des serveurs MCP (Model Context Protocol).
|
||||
//
|
||||
// loki peut se connecter à des serveurs MCP tiers pour enrichir la palette
|
||||
// d'outils de l'IA (accès fichiers rapide type desktop-commander, bases de
|
||||
// données, APIs métier…). Deux transports :
|
||||
//
|
||||
// - stdio : loki lance un process local (command + args) et parle en
|
||||
// JSON-RPC sur stdin/stdout. C'est le cas de desktop-commander, du serveur
|
||||
// filesystem officiel, etc. — lancés via npx/uvx/un binaire.
|
||||
// - http : loki parle à un serveur MCP distant en Streamable HTTP (url +
|
||||
// éventuels en-têtes d'auth).
|
||||
//
|
||||
// Le format du fichier mcp.json reprend celui de Claude Desktop (clé
|
||||
// "mcpServers" indexée par nom) pour que les utilisateurs puissent copier-coller
|
||||
// leurs configs existantes. On ajoute un champ "enabled" par serveur.
|
||||
//
|
||||
// IMPORTANT (sécurité) : un serveur MCP stdio exécute un process arbitraire sur
|
||||
// la machine où tourne loki — même niveau de confiance que l'outil `bash` du
|
||||
// mode agent. La configuration MCP est donc réservée au propriétaire local de la
|
||||
// machine et ne doit JAMAIS être pilotable depuis le relais/accès distant.
|
||||
|
||||
// MCPServerConfig décrit un serveur MCP configuré. Un seul des deux transports
|
||||
// est renseigné : Command => stdio, URL => http.
|
||||
type MCPServerConfig struct {
|
||||
// Transport stdio.
|
||||
Command string `json:"command,omitempty"`
|
||||
Args []string `json:"args,omitempty"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
|
||||
// Transport http (Streamable HTTP).
|
||||
URL string `json:"url,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
|
||||
// Enabled : le serveur n'est connecté et ses outils exposés que s'il est
|
||||
// activé. Un serveur nouvellement ajouté est actif par défaut (voir
|
||||
// UnmarshalJSON) pour coller à l'intuition « je l'ajoute, il marche ».
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// DisabledTools : outils du serveur à NE PAS exposer à l'IA (par nom réel,
|
||||
// non namespacé). Permet de garder un serveur connecté tout en masquant
|
||||
// certains de ses outils. Vide = tous les outils exposés.
|
||||
DisabledTools []string `json:"disabledTools,omitempty"`
|
||||
}
|
||||
|
||||
// ToolDisabled indique si un outil (nom réel) est masqué pour ce serveur.
|
||||
func (c MCPServerConfig) ToolDisabled(tool string) bool {
|
||||
for _, t := range c.DisabledTools {
|
||||
if t == tool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// enabledDefaultTrue est un alias utilisé pour appliquer enabled=true par défaut
|
||||
// quand le champ est absent du JSON (compat configs Claude Desktop sans
|
||||
// "enabled").
|
||||
type mcpServerConfigAlias MCPServerConfig
|
||||
|
||||
// UnmarshalJSON applique enabled=true par défaut lorsque la clé est absente,
|
||||
// pour rester compatible avec les fichiers mcp.json qui ne connaissent pas ce
|
||||
// champ (Claude Desktop, etc.).
|
||||
func (c *MCPServerConfig) UnmarshalJSON(b []byte) error {
|
||||
// Sonde la présence de la clé "enabled".
|
||||
var probe map[string]json.RawMessage
|
||||
if err := json.Unmarshal(b, &probe); err != nil {
|
||||
return err
|
||||
}
|
||||
alias := mcpServerConfigAlias{}
|
||||
if err := json.Unmarshal(b, &alias); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := probe["enabled"]; !ok {
|
||||
alias.Enabled = true
|
||||
}
|
||||
*c = MCPServerConfig(alias)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Transport renvoie "stdio", "http" ou "" (mal configuré : ni command ni url).
|
||||
func (c MCPServerConfig) Transport() string {
|
||||
switch {
|
||||
case strings.TrimSpace(c.Command) != "":
|
||||
return "stdio"
|
||||
case strings.TrimSpace(c.URL) != "":
|
||||
return "http"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Validate vérifie qu'exactement un transport est renseigné.
|
||||
func (c MCPServerConfig) Validate() error {
|
||||
hasCmd := strings.TrimSpace(c.Command) != ""
|
||||
hasURL := strings.TrimSpace(c.URL) != ""
|
||||
switch {
|
||||
case hasCmd && hasURL:
|
||||
return fmt.Errorf("un serveur MCP ne peut avoir à la fois 'command' (stdio) et 'url' (http)")
|
||||
case !hasCmd && !hasURL:
|
||||
return fmt.Errorf("un serveur MCP doit avoir soit 'command' (stdio) soit 'url' (http)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mcpConfigMu sérialise les accès concurrents à la déclaration des serveurs
|
||||
// MCP (l'UI web et les tours de chat peuvent lire/écrire en parallèle).
|
||||
var mcpConfigMu sync.Mutex
|
||||
|
||||
// LoadMCPConfig lit les serveurs MCP déclarés. Aucun => map vide, pas d'erreur.
|
||||
func LoadMCPConfig() (map[string]MCPServerConfig, error) {
|
||||
mcpConfigMu.Lock()
|
||||
defer mcpConfigMu.Unlock()
|
||||
return loadMCPConfigLocked()
|
||||
}
|
||||
|
||||
func loadMCPConfigLocked() (map[string]MCPServerConfig, error) {
|
||||
servers := map[string]MCPServerConfig{}
|
||||
getJSON(bkState, "mcp", &servers)
|
||||
if servers == nil {
|
||||
servers = map[string]MCPServerConfig{}
|
||||
}
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
func saveMCPConfigLocked(servers map[string]MCPServerConfig) error {
|
||||
return putJSON(bkState, "mcp", servers)
|
||||
}
|
||||
|
||||
// SetMCPServer ajoute ou remplace un serveur nommé, puis invalide le pool de
|
||||
// sessions pour que le changement prenne effet au prochain tour.
|
||||
func SetMCPServer(name string, cfg MCPServerConfig) error {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return fmt.Errorf("nom de serveur vide")
|
||||
}
|
||||
if strings.Contains(name, "__") {
|
||||
return fmt.Errorf("le nom ne peut pas contenir '__' (réservé au namespacing des outils)")
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
mcpConfigMu.Lock()
|
||||
servers, err := loadMCPConfigLocked()
|
||||
if err != nil {
|
||||
mcpConfigMu.Unlock()
|
||||
return err
|
||||
}
|
||||
servers[name] = cfg
|
||||
err = saveMCPConfigLocked(servers)
|
||||
mcpConfigMu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mcpInvalidate(name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteMCPServer retire un serveur et ferme sa session si ouverte.
|
||||
func DeleteMCPServer(name string) error {
|
||||
mcpConfigMu.Lock()
|
||||
servers, err := loadMCPConfigLocked()
|
||||
if err != nil {
|
||||
mcpConfigMu.Unlock()
|
||||
return err
|
||||
}
|
||||
if _, ok := servers[name]; !ok {
|
||||
mcpConfigMu.Unlock()
|
||||
return fmt.Errorf("serveur MCP inconnu: %s", name)
|
||||
}
|
||||
delete(servers, name)
|
||||
err = saveMCPConfigLocked(servers)
|
||||
mcpConfigMu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mcpInvalidate(name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetMCPServerEnabled active/désactive un serveur existant.
|
||||
func SetMCPServerEnabled(name string, on bool) error {
|
||||
mcpConfigMu.Lock()
|
||||
servers, err := loadMCPConfigLocked()
|
||||
if err != nil {
|
||||
mcpConfigMu.Unlock()
|
||||
return err
|
||||
}
|
||||
cfg, ok := servers[name]
|
||||
if !ok {
|
||||
mcpConfigMu.Unlock()
|
||||
return fmt.Errorf("serveur MCP inconnu: %s", name)
|
||||
}
|
||||
cfg.Enabled = on
|
||||
servers[name] = cfg
|
||||
err = saveMCPConfigLocked(servers)
|
||||
mcpConfigMu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mcpInvalidate(name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetMCPToolEnabled masque/démasque un outil précis d'un serveur (via sa liste
|
||||
// DisabledTools), puis invalide la session pour recalculer les outils exposés.
|
||||
func SetMCPToolEnabled(server, tool string, on bool) error {
|
||||
mcpConfigMu.Lock()
|
||||
servers, err := loadMCPConfigLocked()
|
||||
if err != nil {
|
||||
mcpConfigMu.Unlock()
|
||||
return err
|
||||
}
|
||||
cfg, ok := servers[server]
|
||||
if !ok {
|
||||
mcpConfigMu.Unlock()
|
||||
return fmt.Errorf("serveur MCP inconnu: %s", server)
|
||||
}
|
||||
// Reconstruit la liste sans l'outil concerné, puis l'ajoute si on désactive.
|
||||
next := cfg.DisabledTools[:0:0]
|
||||
for _, t := range cfg.DisabledTools {
|
||||
if t != tool {
|
||||
next = append(next, t)
|
||||
}
|
||||
}
|
||||
if !on {
|
||||
next = append(next, tool)
|
||||
}
|
||||
cfg.DisabledTools = next
|
||||
servers[server] = cfg
|
||||
err = saveMCPConfigLocked(servers)
|
||||
mcpConfigMu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mcpInvalidate(server)
|
||||
return nil
|
||||
}
|
||||
|
||||
// sortedServerNames renvoie les noms triés, pour un ordre d'affichage/itération
|
||||
// stable.
|
||||
func sortedServerNames(servers map[string]MCPServerConfig) []string {
|
||||
names := make([]string, 0, len(servers))
|
||||
for n := range servers {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
Reference in new issue
Block a user