mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-12 01:37:06 +02:00
- 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.
119 lines
3.6 KiB
Go
119 lines
3.6 KiB
Go
// Package nodewire porte le protocole PARTAGÉ entre le serveur loki et le
|
|
// client léger « poste distant » (loki-node). Zéro dépendance hors stdlib :
|
|
// c'est ce qui permet au client de rester minuscule (il n'embarque ni l'UI, ni
|
|
// llama.cpp, ni bbolt, ni MCP — juste ce fichier + coder/websocket).
|
|
//
|
|
// Le format du fil (Msg + capacités) DOIT rester identique des deux côtés :
|
|
// centraliser ici évite toute dérive entre serveur et client.
|
|
package nodewire
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// Capacités qu'un poste peut exposer. Chacune est un pouvoir concret donné à
|
|
// l'IA sur la machine distante.
|
|
const (
|
|
CapShell = "shell" // exécuter une commande shell
|
|
CapRead = "read" // lire un fichier (confiné à la racine)
|
|
CapWrite = "write" // écrire un fichier (confiné à la racine)
|
|
CapList = "list" // lister un dossier (confiné à la racine)
|
|
)
|
|
|
|
// AllCaps est l'ordre canonique d'affichage/itération.
|
|
var AllCaps = []string{CapShell, CapRead, CapWrite, CapList}
|
|
|
|
// CapLabel donne un libellé court en français.
|
|
func CapLabel(cap string) string {
|
|
switch cap {
|
|
case CapShell:
|
|
return "shell"
|
|
case CapRead:
|
|
return "lire des fichiers"
|
|
case CapWrite:
|
|
return "écrire des fichiers"
|
|
case CapList:
|
|
return "lister des dossiers"
|
|
}
|
|
return cap
|
|
}
|
|
|
|
// Msg est l'unique enveloppe échangée sur le WebSocket, dans les deux sens.
|
|
// - client→serveur au démarrage : {type:"hello", name, os, caps:[…]}
|
|
// - serveur→client : {type:"call", id, cap, args}
|
|
// - client→serveur : {type:"result", id, result}
|
|
type Msg struct {
|
|
Type string `json:"type"`
|
|
ID string `json:"id,omitempty"`
|
|
Cap string `json:"cap,omitempty"`
|
|
Args map[string]any `json:"args,omitempty"`
|
|
Result string `json:"result,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
OS string `json:"os,omitempty"`
|
|
Caps []string `json:"caps,omitempty"`
|
|
}
|
|
|
|
// CapAllowed indique si cap figure dans allowed.
|
|
func CapAllowed(allowed []string, cap string) bool {
|
|
for _, c := range allowed {
|
|
if c == cap {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CapIntersect renvoie, dans l'ordre canonique, les capacités présentes dans les
|
|
// deux listes — le jeu EFFECTIF exposé à l'IA (propriétaire ∩ poste).
|
|
func CapIntersect(allowed, declared []string) []string {
|
|
var out []string
|
|
for _, c := range AllCaps {
|
|
if CapAllowed(allowed, c) && CapAllowed(declared, c) {
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// SanitizeCaps ne garde que des capacités connues, sans doublon, dans l'ordre
|
|
// canonique. Toute entrée inconnue est ignorée.
|
|
func SanitizeCaps(caps []string) []string {
|
|
var out []string
|
|
for _, c := range AllCaps {
|
|
if CapAllowed(caps, c) {
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ResolvePath résout un chemin demandé À L'INTÉRIEUR de la racine autorisée et
|
|
// REFUSE tout ce qui en sortirait (traversée « .. », absolu hors racine). C'est
|
|
// le confinement des outils fichiers.
|
|
func ResolvePath(root, p string) (string, error) {
|
|
if strings.TrimSpace(root) == "" {
|
|
return "", errors.New("aucun dossier racine configuré sur ce poste")
|
|
}
|
|
if strings.TrimSpace(p) == "" {
|
|
return "", errors.New("chemin vide")
|
|
}
|
|
root = filepath.Clean(root)
|
|
var abs string
|
|
if filepath.IsAbs(p) {
|
|
abs = filepath.Clean(p)
|
|
} else {
|
|
abs = filepath.Clean(filepath.Join(root, p))
|
|
}
|
|
rel, err := filepath.Rel(root, abs)
|
|
if err != nil {
|
|
return "", fmt.Errorf("chemin invalide: %v", err)
|
|
}
|
|
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
|
return "", fmt.Errorf("chemin hors du dossier autorisé (%s)", root)
|
|
}
|
|
return abs, nil
|
|
}
|