mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-24 15:38:12 +02:00
v0.8.9 : piloter un autre PC depuis l'IA (poste distant ajean-remote)
This commit is contained in:
1 parent
6d36464971
commit
7fa5461136
27 files changed
+2432
-49
No files matched your search
@@ -0,0 +1,118 @@
|
||||
// Package nodewire porte le protocole PARTAGÉ entre le serveur ajean et le
|
||||
// client léger « poste distant » (ajean-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
|
||||
}
|
||||
Reference in new issue
Block a user