mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Portage des v0.9.9 → v0.10.2 de l'amont, la dernière vraie fonctionnalité qui nous manquait. Une consigne, une fréquence (« @every 2h », « tous les jours à 9h », ou une expression cron à 5 champs, dans le fuseau du navigateur), et l'IA l'exécute seule en arrière-plan. Preset épinglé par tâche (bascule de modèle avant l'exécution, attente du rechargement), accès mémoire et web réglables, interrupteur maître pour tout suspendre, bouton « tester maintenant ». Le planificateur est une goroutine, un tic par minute, dans le process qui détient la conversation et parle au moteur. Une seule tâche part par tic : de toute façon une seule inférence tourne à la fois, et étaler les départs évite qu'une rafale monopolise le modèle. Occupé ou modèle en cours de chargement n'est pas un échec — la tâche repasse au tic suivant. Une tâche ne partage QU'UN point avec le chat : le verrou de génération. Ni messages, ni journal d'affichage, ni epoch — elle construit son fil éphémère et le jette, ne gardant que son texte final comme compte-rendu (borné à 4000 caractères, réinjecté au passage suivant pour la continuité). Deux adaptations, parce que loki n'est pas l'amont : — Dossier de travail. Ici il appartient à la DISCUSSION ouverte : une tâche y aurait déposé ses fichiers, et en aurait changé en cours de route si l'utilisateur changeait de discussion — pour disparaître avec elle à la suppression. Chaque tâche a donc le sien (workspace/tasks/<id>/), stable d'un passage à l'autre. La bascule ne touche que les points d'entrée des OUTILS (agentCwd) : le panneau Fichiers, les dépôts et les liens des messages continuent de suivre la discussion de l'utilisateur. — Capacités. Mem est posé explicitement à MemOff quand l'agent est coupé : le zéro de MemMode est la chaîne vide, qu'EnabledTools ne reconnaît pas comme « coupée » — une tâche sans agent se serait vu offrir les outils mem_*. Le mode code reste off : rôles, critères et passe de vérification n'ont pas de sens sans personne en face. Le refus d'un message pendant qu'une tâche tourne dit maintenant LAQUELLE occupe le modèle : « génération en cours » sur un fil vide et immobile n'expliquait rien. Interface : section repliable dans les réglages (liste, état, prochain passage, pastille), modale d'édition bâtie sur le gabarit de l'éditeur de preset, panneau « dernier résultat » en markdown. Vérifié dans un vrai navigateur — création, rendu, réouverture en édition, bascule intervalle/cron, interrupteur maître, suppression — sans une seule erreur JS.
161 lines
6.6 KiB
Go
161 lines
6.6 KiB
Go
package loki
|
|
|
|
// code_policy.go — politique d'exécution des outils de l'agent (repris dans
|
|
// l'esprit de tool-policy/path-security d'OpenFox, réécrit en Go — voir
|
|
// NOTICE.md).
|
|
//
|
|
// Deux gardes indépendantes :
|
|
// - dangerousCommand : liste courte de commandes CATASTROPHIQUES (effacement
|
|
// de disque, arrêt machine, fork bomb). Toujours active, mode chat compris :
|
|
// aucune de ces commandes n'a d'usage légitime lancée par un modèle. Le
|
|
// refus explique quoi faire (l'utilisateur la lance lui-même).
|
|
// - codePathAllowed : en MODE CODE, tous les chemins des outils fichiers sont
|
|
// bornés au dossier de la discussion — liens symboliques résolus des deux
|
|
// côtés, comme le panneau Fichiers (relWithin). Le mode chat garde le
|
|
// comportement historique (chemins absolus honorés) : c'est le mode code
|
|
// qui promet un bac à sable, pas l'agent généraliste.
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// dangerousPatterns : motifs (insensibles à la casse) de commandes refusées.
|
|
// Volontairement ÉTROIT : viser la perte de données irréversible et l'arrêt de
|
|
// la machine, pas la moindre écriture. Un motif trop large rend l'agent
|
|
// inutilisable et pousse à tout désactiver.
|
|
var dangerousPatterns = []struct {
|
|
re *regexp.Regexp
|
|
reason string
|
|
}{
|
|
// rm -rf / (ou ~, ou une racine de lecteur) — pas un rm -rf d'un sous-dossier.
|
|
{regexp.MustCompile(`(?i)\brm\s+(-[a-z]*r[a-z]*f[a-z]*|-[a-z]*f[a-z]*r[a-z]*)\s+("?/"?|~/?|/\*|[a-z]:\\?)\s*$`), "efface la racine du disque ou le home"},
|
|
{regexp.MustCompile(`(?i)\brm\s+(-[a-z]*r[a-z]*f[a-z]*|-[a-z]*f[a-z]*r[a-z]*)\s+("?/"?|~/?|/\*|[a-z]:\\?)\s`), "efface la racine du disque ou le home"},
|
|
// Écriture directe sur un périphérique bloc / formatage.
|
|
{regexp.MustCompile(`(?i)\bmkfs(\.[a-z0-9]+)?\b`), "formate un système de fichiers"},
|
|
{regexp.MustCompile(`(?i)\bdd\b[^\n]*\bof=/dev/`), "écrit directement sur un périphérique disque"},
|
|
{regexp.MustCompile(`(?i)>\s*/dev/sd[a-z]\b`), "écrit directement sur un périphérique disque"},
|
|
// Arrêt / redémarrage de la machine (ici : le conteneur, donc tout Loki).
|
|
// Ancré en position de COMMANDE (début, après ; && | ou sudo) : le mot
|
|
// « reboot » dans un grep ou un texte n'est pas une commande.
|
|
{regexp.MustCompile(`(?i)(^|[;&|]\s*|\bsudo\s+)(shutdown|poweroff|reboot|halt)\b`), "arrête ou redémarre la machine"},
|
|
// Fork bomb classique.
|
|
{regexp.MustCompile(`:\(\)\s*\{\s*:\|:`), "fork bomb"},
|
|
// chmod/chown récursif sur la racine.
|
|
{regexp.MustCompile(`(?i)\bch(mod|own)\b[^\n]*-[a-z]*R[a-z]*\s+[^\s]+\s+/\s*$`), "change récursivement les droits de la racine"},
|
|
// Windows : suppression récursive de la racine d'un lecteur, format.
|
|
{regexp.MustCompile(`(?i)\b(rd|rmdir)\s+/s\b[^\n]*\s[a-z]:\\?\s*$`), "efface la racine d'un lecteur"},
|
|
{regexp.MustCompile(`(?i)\bdel\s+(/[fsq]\s+)*[a-z]:\\\*`), "efface la racine d'un lecteur"},
|
|
{regexp.MustCompile(`(?i)\bformat\s+[a-z]:`), "formate un lecteur"},
|
|
}
|
|
|
|
// dangerousCommand renvoie la raison du refus si la commande correspond à un
|
|
// motif interdit, "" sinon.
|
|
func dangerousCommand(cmd string) string {
|
|
for _, p := range dangerousPatterns {
|
|
if p.re.MatchString(cmd) {
|
|
return p.reason
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// refusedCommandResult formate le refus renvoyé au modèle : dire POURQUOI et
|
|
// QUOI FAIRE, sinon il réessaie en boucle avec des variantes.
|
|
func refusedCommandResult(reason string) string {
|
|
return "[refusé] Commande bloquée par la politique de sécurité : " + reason + ". " +
|
|
"Ne la réessaie pas, même reformulée. Si l'utilisateur la veut vraiment, dis-lui de la lancer lui-même dans un terminal."
|
|
}
|
|
|
|
// codePathAllowed dit si un chemin (déjà résolu par resolveAgentPath) est DANS
|
|
// le dossier de la discussion courante. Utilisé par les outils fichiers en mode
|
|
// code uniquement.
|
|
func codePathAllowed(abs string) bool {
|
|
ws := agentCwd()
|
|
if ws == "" {
|
|
return false
|
|
}
|
|
abs = filepath.Clean(abs)
|
|
// Le fichier peut ne pas exister encore (write) : EvalSymlinks échouerait.
|
|
// On teste alors son ancêtre le plus proche existant — c'est LUI qui doit
|
|
// être dans le workspace, liens symboliques résolus.
|
|
probe := abs
|
|
for {
|
|
if _, err := os.Stat(probe); err == nil {
|
|
break
|
|
}
|
|
parent := filepath.Dir(probe)
|
|
if parent == probe {
|
|
break
|
|
}
|
|
probe = parent
|
|
}
|
|
if !sameOrWithin(ws, probe) {
|
|
return false
|
|
}
|
|
// L'ancêtre est dans le workspace ; le chemin FINAL ne doit pas en
|
|
// ressortir par des ".." lexicaux non encore résolus.
|
|
rel, err := filepath.Rel(ws, abs)
|
|
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// sameOrWithin : p est root lui-même, ou dedans — liens symboliques résolus
|
|
// des deux côtés (relWithin refuse « . », d'où ce cousin qui l'accepte : la
|
|
// racine du workspace est un chemin légitime pour grep/glob).
|
|
func sameOrWithin(root, p string) bool {
|
|
if r, err := filepath.EvalSymlinks(root); err == nil {
|
|
root = r
|
|
}
|
|
if q, err := filepath.EvalSymlinks(p); err == nil {
|
|
p = q
|
|
}
|
|
rel, err := filepath.Rel(root, p)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)))
|
|
}
|
|
|
|
// refusedPathResult : refus de chemin hors du bac à sable du mode code.
|
|
func refusedPathResult(path string) string {
|
|
return "[refusé] " + path + " est hors du dossier de cette discussion. " +
|
|
"En mode code, tous les fichiers vivent dans le dossier de travail — utilise des chemins relatifs."
|
|
}
|
|
|
|
// codeWriteGuard regroupe les gardes du mode code AVANT une écriture locale
|
|
// (write/edit) : borne de chemin, puis tracker « lu avant d'écrire ».
|
|
// Renvoie "" si tout est bon, sinon le message de refus pour le modèle.
|
|
// En mode chat, aucune garde : comportement historique de l'agent.
|
|
func codeWriteGuard(caps Caps, file string, forWrite bool) string {
|
|
if !caps.Code {
|
|
return ""
|
|
}
|
|
path := resolveAgentPath(file)
|
|
if !codePathAllowed(path) {
|
|
return refusedPathResult(file)
|
|
}
|
|
return trackerCheck(path, forWrite)
|
|
}
|
|
|
|
// --- Mutex par fichier -------------------------------------------------------
|
|
// Sérialise les écritures concurrentes sur un même fichier (write/edit). Sans
|
|
// parallélisme d'outils aujourd'hui le coût est nul, mais le jour où deux tours
|
|
// ou un job d'arrière-plan écrivent le même fichier, la corruption est évitée.
|
|
|
|
var fileMuMap sync.Map // clé : chemin nettoyé → *sync.Mutex
|
|
|
|
func fileMu(path string) *sync.Mutex {
|
|
key := filepath.Clean(path)
|
|
if m, ok := fileMuMap.Load(key); ok {
|
|
return m.(*sync.Mutex)
|
|
}
|
|
m, _ := fileMuMap.LoadOrStore(key, &sync.Mutex{})
|
|
return m.(*sync.Mutex)
|
|
}
|