mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Deux reprises d'AJEAN autour des tâches planifiées. Dossier de scripts (/data/scripts) : un dossier durable À CÔTÉ de memory, hors du workspace. Le workspace est jetable — supprimer une discussion emporte ses fichiers — donc un script qu'on veut garder n'y avait pas sa place. Le briefing machine l'annonce à l'IA, qui y écrit et y lance ses scripts normalement. Tâche « script seul » (Task.Kind/Script) : le planificateur lance le script sans charger le modèle ni consommer un token, sa sortie devient le compte-rendu, et l'UI l'affiche comme n'importe quelle tâche. Elle tourne hors du verrou de génération — d'où un registre à part pour l'afficher « en cours » et l'arrêter (/api/tasks/stop), et un « tester » qui n'attend ni le verrou ni le moteur. Sélecteur Consigne IA / Script seul dans la modale. Outils task_list/create/update/delete : l'IA se donne elle-même rappels et veilles récurrentes, cloisonnés par projet (dans un projet, elle ne voit et ne pilote que ses tâches). Le budget du préambule passe de 8600 à 11000 caractères, avec le palier documenté dans le test. Dossier mémoire réservé à ses outils : bash, write et edit ne le touchent plus (guardToolOnly*). Un `cat memory/…` contournait l'index MEMORY.md, et c'est la condition d'un chiffrement de la mémoire à venir. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K6CAgoLJzufeA8rZTSpSpY
104 lines
3.2 KiB
Go
104 lines
3.2 KiB
Go
package loki
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestGuardToolOnly vérifie que memory est inaccessible en direct (shell/write/
|
|
// edit) et n'autorise que les outils dédiés, tandis que scripts reste libre.
|
|
func TestGuardToolOnly(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("LOKI_HOME", home)
|
|
|
|
// Shell : toute mention du dossier memory est refusée (lecture comprise). Le
|
|
// dossier scripts, lui, est librement utilisable (l'IA y écrit et exécute).
|
|
blockedCmds := []string{
|
|
"cat " + memoryDir() + "/note.md",
|
|
"echo x > " + memoryDir() + "/x",
|
|
}
|
|
for _, c := range blockedCmds {
|
|
if msg := guardToolOnlyCommand(c); msg == "" {
|
|
t.Errorf("commande aurait dû être refusée : %q", c)
|
|
}
|
|
}
|
|
allowedCmds := []string{
|
|
"ls " + workspaceDir(),
|
|
"bash " + scriptsDir() + "/backup.sh", // exécuter un script : autorisé
|
|
"ls " + scriptsDir(),
|
|
}
|
|
for _, c := range allowedCmds {
|
|
if msg := guardToolOnlyCommand(c); msg != "" {
|
|
t.Errorf("commande ne devait pas être bloquée : %q -> %s", c, msg)
|
|
}
|
|
}
|
|
|
|
// write/edit : un chemin dans memory est refusé ; dans scripts ou workspace, non.
|
|
if msg := guardToolOnlyPath(filepath.Join(memoryDir(), "a.md")); msg == "" {
|
|
t.Errorf("écriture directe dans memory aurait dû être refusée")
|
|
}
|
|
for _, p := range []string{filepath.Join(scriptsDir(), "b.sh"), filepath.Join(workspaceDir(), "ok.txt")} {
|
|
if msg := guardToolOnlyPath(p); msg != "" {
|
|
t.Errorf("écriture ne devait pas être bloquée : %q -> %s", p, msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestScriptsPathEscape vérifie qu'on ne peut pas sortir du dossier scripts ni
|
|
// glisser un caractère hostile au shell dans le nom.
|
|
func TestScriptsPathEscape(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("LOKI_HOME", home)
|
|
|
|
bad := []string{"../evil.sh", "..", "/etc/passwd", `..\..\x`, `a";rm -rf ~;".sh`, "a$b.sh", "a`b`.sh"}
|
|
for _, name := range bad {
|
|
if _, err := scriptsPath(name); err == nil {
|
|
t.Errorf("aurait dû refuser le nom : %q", name)
|
|
}
|
|
}
|
|
good := []string{"backup.ps1", "sub/backup.sh", "a.sh", "my backup.sh"}
|
|
for _, name := range good {
|
|
p, err := scriptsPath(name)
|
|
if err != nil {
|
|
t.Errorf("aurait dû accepter %q : %v", name, err)
|
|
continue
|
|
}
|
|
if !strings.HasPrefix(normPath(p), normPath(scriptsDir())) {
|
|
t.Errorf("%q résolu hors de scriptsDir : %s", name, p)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestScriptExists couvre la validation d'existence utilisée par les tâches script.
|
|
func TestScriptExists(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("LOKI_HOME", home)
|
|
|
|
if err := scriptExists("hello.sh"); err == nil {
|
|
t.Fatalf("un script absent aurait dû échouer")
|
|
}
|
|
full := filepath.Join(scriptsDir(), "hello.sh")
|
|
if err := os.MkdirAll(scriptsDir(), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(full, []byte("#!/bin/sh\necho hi\n"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := scriptExists("hello.sh"); err != nil {
|
|
t.Fatalf("scriptExists : %v", err)
|
|
}
|
|
list, _ := listScripts()
|
|
if len(list) != 1 || list[0].Name != "hello.sh" {
|
|
t.Fatalf("list inattendu : %+v", list)
|
|
}
|
|
// Un dossier n'est pas un script.
|
|
if err := os.MkdirAll(filepath.Join(scriptsDir(), "d"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := scriptExists("d"); err == nil {
|
|
t.Fatalf("un dossier ne doit pas passer pour un script")
|
|
}
|
|
}
|