mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-16 03:39:29 +02:00
Chat : l'exécution des outils sort de runChatLoop (execTool)
Lot 6, étape 1, sans changement de comportement : le switch des outils de la boucle de chat (une cinquantaine d'outils) devient execTool, extrait tel quel ; seule la question « ask », qui émettait un événement, le rend désormais à l'appelant qui l'émet au même moment. C'est la frontière par laquelle une boucle de chat écrite en Rust fera exécuter ses outils par le Go tant qu'ils ne seront pas portés : API interne /internal/v1/tool/exec, même fonction, donc mêmes garde-fous (commande dangereuse refusée avant exécution, cible distante, écriture hors du dossier de travail) ; la coupure de la connexion interrompt la commande comme le bouton stop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
99e456b9a2
commit
c744b744ee
4 files changed
+332
-186
No files matched your search
+215
-186
@@ -2360,192 +2360,10 @@ func runChatLoop(ctx context.Context, kt *keepImages, messages []Message, tools
|
||||
extra = append(extra, toolMsg)
|
||||
continue
|
||||
}
|
||||
switch tc.Function.Name {
|
||||
case "see_image":
|
||||
result, visionImg = toolSeeImage(label)
|
||||
case "recall":
|
||||
result = toolRecall(args)
|
||||
case "recall_search":
|
||||
result = toolRecallSearch(args)
|
||||
case "tracker":
|
||||
result = toolTracker(args)
|
||||
case "mem_search":
|
||||
lim := 0
|
||||
if v, ok := args["limit"].(float64); ok {
|
||||
lim = int(v)
|
||||
}
|
||||
hits := MemSearch(label, lim)
|
||||
if len(hits) == 0 {
|
||||
result = "[aucun résultat]"
|
||||
} else {
|
||||
var b strings.Builder
|
||||
for _, h := range hits {
|
||||
fmt.Fprintf(&b, "- %s — %s\n %s\n", h.File, h.Title, h.Snippet)
|
||||
}
|
||||
result = strings.TrimRight(b.String(), "\n")
|
||||
}
|
||||
case "mem_read":
|
||||
off, lim := 0, 0
|
||||
if v, ok := args["offset"].(float64); ok {
|
||||
off = int(v)
|
||||
}
|
||||
if v, ok := args["limit"].(float64); ok {
|
||||
lim = int(v)
|
||||
}
|
||||
if c, rerr := MemRead(label, off, lim); rerr != nil {
|
||||
result = "[erreur] " + rerr.Error()
|
||||
} else {
|
||||
result = c
|
||||
}
|
||||
case "mem_add":
|
||||
content, _ := args["content"].(string)
|
||||
if werr := MemAdd(label, content); werr != nil {
|
||||
result = "[erreur] " + werr.Error()
|
||||
} else {
|
||||
result = fmt.Sprintf("[ok] page '%s' créée", label)
|
||||
diff, diffAdd = addedDiff(content)
|
||||
}
|
||||
case "mem_edit":
|
||||
oldText, _ := args["old"].(string)
|
||||
newText, _ := args["new"].(string)
|
||||
if werr := MemEdit(label, oldText, newText); errors.Is(werr, errAlreadyApplied) {
|
||||
result = fmt.Sprintf("[ok] page '%s' %s", label, werr.Error())
|
||||
} else if werr != nil {
|
||||
result = "[erreur] " + werr.Error()
|
||||
} else {
|
||||
result = fmt.Sprintf("[ok] page '%s' modifiée", label)
|
||||
diff, diffAdd, diffDel = lineDiff(oldText, newText)
|
||||
}
|
||||
case "write":
|
||||
content, _ := args["content"].(string)
|
||||
if tgt := agentTargetSlug(); tgt != "" {
|
||||
// Cible = un poste distant : on écrit LÀ-BAS. Pas de diff (on
|
||||
// n'a pas l'ancien contenu du fichier distant).
|
||||
result = nodeCall(tgt, nodeCapWrite, map[string]any{"path": label, "content": content})
|
||||
} else if msg := codeWriteGuard(caps, label, true); msg != "" {
|
||||
result = msg
|
||||
} else {
|
||||
result = fileWrite(label, content)
|
||||
if !strings.HasPrefix(result, "[erreur]") {
|
||||
diff, diffAdd = addedDiff(content)
|
||||
trackerNoteWrite(resolveAgentPath(label))
|
||||
result += lspDiagBlock(resolveAgentPath(label), caps)
|
||||
}
|
||||
}
|
||||
case "edit":
|
||||
oldText, _ := args["old"].(string)
|
||||
newText, _ := args["new"].(string)
|
||||
if tgt := agentTargetSlug(); tgt != "" {
|
||||
result = nodeEditRemote(tgt, label, oldText, newText)
|
||||
} else if msg := codeWriteGuard(caps, label, false); msg != "" {
|
||||
result = msg
|
||||
} else {
|
||||
result = fileEdit(label, oldText, newText)
|
||||
// Diff seulement si l'édition a réussi (sinon le fichier n'a pas bougé).
|
||||
if !strings.HasPrefix(result, "[erreur]") {
|
||||
diff, diffAdd, diffDel = lineDiff(oldText, newText)
|
||||
trackerNoteWrite(resolveAgentPath(label))
|
||||
result += lspDiagBlock(resolveAgentPath(label), caps)
|
||||
}
|
||||
}
|
||||
case "read":
|
||||
result = toolRead(args, caps.Code)
|
||||
case "grep":
|
||||
result = toolGrep(args, caps.Code)
|
||||
case "glob":
|
||||
result = toolGlob(args, caps.Code)
|
||||
case "ask":
|
||||
result = toolAsk(args)
|
||||
if !strings.HasPrefix(result, "[erreur]") {
|
||||
q, _ := args["question"].(string)
|
||||
cb(StreamEvent{Ask: &AskEvent{Question: q, Options: askOptions(args)}})
|
||||
}
|
||||
case "bash_bg":
|
||||
result = toolBashBg(args)
|
||||
case "bash_tail":
|
||||
result = toolBashTail(args)
|
||||
case "git_status":
|
||||
result = toolGitStatus(ctx, args)
|
||||
case "git_diff":
|
||||
result = toolGitDiff(ctx, args)
|
||||
case "git_clone":
|
||||
result = toolGitClone(ctx, args)
|
||||
case "criteria":
|
||||
result = toolCriteria(args, caps.Role == "verifier")
|
||||
case "subagent":
|
||||
result = toolSubagent(ctx, args, caps)
|
||||
case "bash":
|
||||
to := 0
|
||||
switch v := args["timeout"].(type) {
|
||||
case float64:
|
||||
to = int(v)
|
||||
case int:
|
||||
to = v
|
||||
}
|
||||
// Politique de sécurité : les commandes catastrophiques sont
|
||||
// refusées AVANT toute exécution, cible distante comprise.
|
||||
if reason := dangerousCommand(label); reason != "" {
|
||||
result = refusedCommandResult(reason)
|
||||
break
|
||||
}
|
||||
// Mode Code : « cmd & » laisse un process orphelin, sans sortie
|
||||
// lisible ni moyen de l'arrêter — bash_bg est fait pour ça.
|
||||
if caps.Code && trailingBackground(label) {
|
||||
result = "[refusé] commande terminée par « & » : lance-la avec bash_bg (sortie consultable avec bash_tail), pas en arrière-plan dans bash."
|
||||
break
|
||||
}
|
||||
if tgt := agentTargetSlug(); tgt != "" {
|
||||
// Cible = un poste distant : la commande s'exécute LÀ-BAS via le
|
||||
// canal du poste (fail-closed : nodeCall renvoie une erreur si le
|
||||
// poste est déconnecté, on n'exécute JAMAIS sur le serveur à sa place).
|
||||
result = nodeCall(tgt, nodeCapShell, map[string]any{"command": label, "timeout": to})
|
||||
} else {
|
||||
result = runShell(ctx, label, to)
|
||||
}
|
||||
case "web_search":
|
||||
result = capWebOutput(toolWebSearch(args))
|
||||
case "web_images":
|
||||
result = capWebOutput(toolWebImages(args))
|
||||
case "web_open":
|
||||
result = capWebOutput(toolWebOpen(args))
|
||||
case "web_read":
|
||||
result = capWebOutput(toolWebRead(args))
|
||||
case "web_grep":
|
||||
result = capWebOutput(toolWebGrep(args))
|
||||
case "web_screenshot":
|
||||
result = toolWebScreenshot(args, caps)
|
||||
case "task_list":
|
||||
result = toolTaskList(args)
|
||||
case "task_create":
|
||||
result = toolTaskCreate(args)
|
||||
case "task_update":
|
||||
result = toolTaskUpdate(args)
|
||||
case "task_delete":
|
||||
result = toolTaskDelete(args)
|
||||
case "browser_open":
|
||||
result = capCUOutput(toolCUOpen(args))
|
||||
case "browser_snapshot":
|
||||
result = capCUOutput(toolCUSnapshot())
|
||||
case "browser_find":
|
||||
result = capCUOutput(toolCUFind(args))
|
||||
case "browser_click":
|
||||
result = capCUOutput(toolCUClick(args))
|
||||
case "browser_click_xy":
|
||||
result = capCUOutput(toolCUClickXY(args))
|
||||
case "browser_type":
|
||||
result = capCUOutput(toolCUType(args))
|
||||
case "browser_key":
|
||||
result = capCUOutput(toolCUKey(args))
|
||||
case "browser_scroll":
|
||||
result = capCUOutput(toolCUScroll(args))
|
||||
case "browser_screenshot":
|
||||
result, visionImg = toolCUScreenshot()
|
||||
default:
|
||||
if isMCPTool(tc.Function.Name) {
|
||||
result = mcpCall(tc.Function.Name, args)
|
||||
} else {
|
||||
result = "[erreur] outil inconnu: " + tc.Function.Name
|
||||
}
|
||||
ex := execTool(ctx, tc.Function.Name, args, label, caps)
|
||||
result, diff, diffAdd, diffDel, visionImg = ex.Result, ex.Diff, ex.Added, ex.Removed, ex.VisionImg
|
||||
if ex.Ask != nil {
|
||||
cb(StreamEvent{Ask: ex.Ask})
|
||||
}
|
||||
// Aperçu en direct du navigateur piloté (repris d'AJEAN) : une
|
||||
// capture après chaque action qui change la page, pour l'UI seule.
|
||||
@@ -2773,3 +2591,214 @@ func healthCheck() bool {
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
return resp.StatusCode == 200
|
||||
}
|
||||
|
||||
// toolExecResult : ce que produit l'exécution d'un appel d'outil, hors
|
||||
// événements d'interface (que l'appelant émet).
|
||||
type toolExecResult struct {
|
||||
Result string `json:"result"`
|
||||
Diff []DiffLine `json:"diff,omitempty"`
|
||||
Added int `json:"added,omitempty"`
|
||||
Removed int `json:"removed,omitempty"`
|
||||
VisionImg map[string]any `json:"vision_img,omitempty"`
|
||||
Ask *AskEvent `json:"ask,omitempty"`
|
||||
}
|
||||
|
||||
// execTool exécute UN appel d'outil. Extrait tel quel de runChatLoop (lot 6
|
||||
// de la migration Rust) : c'est la frontière par laquelle une boucle de chat
|
||||
// écrite en Rust fera exécuter ses outils par ce process, tant qu'ils n'auront
|
||||
// pas été portés (API interne /internal/v1/tool/exec). label = toolCallLabel.
|
||||
func execTool(ctx context.Context, name string, args map[string]any, label string, caps Caps) toolExecResult {
|
||||
result := ""
|
||||
var diff []DiffLine
|
||||
var diffAdd, diffDel int
|
||||
var visionImg map[string]any
|
||||
var ask *AskEvent
|
||||
switch name {
|
||||
case "see_image":
|
||||
result, visionImg = toolSeeImage(label)
|
||||
case "recall":
|
||||
result = toolRecall(args)
|
||||
case "recall_search":
|
||||
result = toolRecallSearch(args)
|
||||
case "tracker":
|
||||
result = toolTracker(args)
|
||||
case "mem_search":
|
||||
lim := 0
|
||||
if v, ok := args["limit"].(float64); ok {
|
||||
lim = int(v)
|
||||
}
|
||||
hits := MemSearch(label, lim)
|
||||
if len(hits) == 0 {
|
||||
result = "[aucun résultat]"
|
||||
} else {
|
||||
var b strings.Builder
|
||||
for _, h := range hits {
|
||||
fmt.Fprintf(&b, "- %s — %s\n %s\n", h.File, h.Title, h.Snippet)
|
||||
}
|
||||
result = strings.TrimRight(b.String(), "\n")
|
||||
}
|
||||
case "mem_read":
|
||||
off, lim := 0, 0
|
||||
if v, ok := args["offset"].(float64); ok {
|
||||
off = int(v)
|
||||
}
|
||||
if v, ok := args["limit"].(float64); ok {
|
||||
lim = int(v)
|
||||
}
|
||||
if c, rerr := MemRead(label, off, lim); rerr != nil {
|
||||
result = "[erreur] " + rerr.Error()
|
||||
} else {
|
||||
result = c
|
||||
}
|
||||
case "mem_add":
|
||||
content, _ := args["content"].(string)
|
||||
if werr := MemAdd(label, content); werr != nil {
|
||||
result = "[erreur] " + werr.Error()
|
||||
} else {
|
||||
result = fmt.Sprintf("[ok] page '%s' créée", label)
|
||||
diff, diffAdd = addedDiff(content)
|
||||
}
|
||||
case "mem_edit":
|
||||
oldText, _ := args["old"].(string)
|
||||
newText, _ := args["new"].(string)
|
||||
if werr := MemEdit(label, oldText, newText); errors.Is(werr, errAlreadyApplied) {
|
||||
result = fmt.Sprintf("[ok] page '%s' %s", label, werr.Error())
|
||||
} else if werr != nil {
|
||||
result = "[erreur] " + werr.Error()
|
||||
} else {
|
||||
result = fmt.Sprintf("[ok] page '%s' modifiée", label)
|
||||
diff, diffAdd, diffDel = lineDiff(oldText, newText)
|
||||
}
|
||||
case "write":
|
||||
content, _ := args["content"].(string)
|
||||
if tgt := agentTargetSlug(); tgt != "" {
|
||||
// Cible = un poste distant : on écrit LÀ-BAS. Pas de diff (on
|
||||
// n'a pas l'ancien contenu du fichier distant).
|
||||
result = nodeCall(tgt, nodeCapWrite, map[string]any{"path": label, "content": content})
|
||||
} else if msg := codeWriteGuard(caps, label, true); msg != "" {
|
||||
result = msg
|
||||
} else {
|
||||
result = fileWrite(label, content)
|
||||
if !strings.HasPrefix(result, "[erreur]") {
|
||||
diff, diffAdd = addedDiff(content)
|
||||
trackerNoteWrite(resolveAgentPath(label))
|
||||
result += lspDiagBlock(resolveAgentPath(label), caps)
|
||||
}
|
||||
}
|
||||
case "edit":
|
||||
oldText, _ := args["old"].(string)
|
||||
newText, _ := args["new"].(string)
|
||||
if tgt := agentTargetSlug(); tgt != "" {
|
||||
result = nodeEditRemote(tgt, label, oldText, newText)
|
||||
} else if msg := codeWriteGuard(caps, label, false); msg != "" {
|
||||
result = msg
|
||||
} else {
|
||||
result = fileEdit(label, oldText, newText)
|
||||
// Diff seulement si l'édition a réussi (sinon le fichier n'a pas bougé).
|
||||
if !strings.HasPrefix(result, "[erreur]") {
|
||||
diff, diffAdd, diffDel = lineDiff(oldText, newText)
|
||||
trackerNoteWrite(resolveAgentPath(label))
|
||||
result += lspDiagBlock(resolveAgentPath(label), caps)
|
||||
}
|
||||
}
|
||||
case "read":
|
||||
result = toolRead(args, caps.Code)
|
||||
case "grep":
|
||||
result = toolGrep(args, caps.Code)
|
||||
case "glob":
|
||||
result = toolGlob(args, caps.Code)
|
||||
case "ask":
|
||||
result = toolAsk(args)
|
||||
if !strings.HasPrefix(result, "[erreur]") {
|
||||
q, _ := args["question"].(string)
|
||||
ask = &AskEvent{Question: q, Options: askOptions(args)}
|
||||
}
|
||||
case "bash_bg":
|
||||
result = toolBashBg(args)
|
||||
case "bash_tail":
|
||||
result = toolBashTail(args)
|
||||
case "git_status":
|
||||
result = toolGitStatus(ctx, args)
|
||||
case "git_diff":
|
||||
result = toolGitDiff(ctx, args)
|
||||
case "git_clone":
|
||||
result = toolGitClone(ctx, args)
|
||||
case "criteria":
|
||||
result = toolCriteria(args, caps.Role == "verifier")
|
||||
case "subagent":
|
||||
result = toolSubagent(ctx, args, caps)
|
||||
case "bash":
|
||||
to := 0
|
||||
switch v := args["timeout"].(type) {
|
||||
case float64:
|
||||
to = int(v)
|
||||
case int:
|
||||
to = v
|
||||
}
|
||||
// Politique de sécurité : les commandes catastrophiques sont
|
||||
// refusées AVANT toute exécution, cible distante comprise.
|
||||
if reason := dangerousCommand(label); reason != "" {
|
||||
result = refusedCommandResult(reason)
|
||||
break
|
||||
}
|
||||
// Mode Code : « cmd & » laisse un process orphelin, sans sortie
|
||||
// lisible ni moyen de l'arrêter — bash_bg est fait pour ça.
|
||||
if caps.Code && trailingBackground(label) {
|
||||
result = "[refusé] commande terminée par « & » : lance-la avec bash_bg (sortie consultable avec bash_tail), pas en arrière-plan dans bash."
|
||||
break
|
||||
}
|
||||
if tgt := agentTargetSlug(); tgt != "" {
|
||||
// Cible = un poste distant : la commande s'exécute LÀ-BAS via le
|
||||
// canal du poste (fail-closed : nodeCall renvoie une erreur si le
|
||||
// poste est déconnecté, on n'exécute JAMAIS sur le serveur à sa place).
|
||||
result = nodeCall(tgt, nodeCapShell, map[string]any{"command": label, "timeout": to})
|
||||
} else {
|
||||
result = runShell(ctx, label, to)
|
||||
}
|
||||
case "web_search":
|
||||
result = capWebOutput(toolWebSearch(args))
|
||||
case "web_images":
|
||||
result = capWebOutput(toolWebImages(args))
|
||||
case "web_open":
|
||||
result = capWebOutput(toolWebOpen(args))
|
||||
case "web_read":
|
||||
result = capWebOutput(toolWebRead(args))
|
||||
case "web_grep":
|
||||
result = capWebOutput(toolWebGrep(args))
|
||||
case "web_screenshot":
|
||||
result = toolWebScreenshot(args, caps)
|
||||
case "task_list":
|
||||
result = toolTaskList(args)
|
||||
case "task_create":
|
||||
result = toolTaskCreate(args)
|
||||
case "task_update":
|
||||
result = toolTaskUpdate(args)
|
||||
case "task_delete":
|
||||
result = toolTaskDelete(args)
|
||||
case "browser_open":
|
||||
result = capCUOutput(toolCUOpen(args))
|
||||
case "browser_snapshot":
|
||||
result = capCUOutput(toolCUSnapshot())
|
||||
case "browser_find":
|
||||
result = capCUOutput(toolCUFind(args))
|
||||
case "browser_click":
|
||||
result = capCUOutput(toolCUClick(args))
|
||||
case "browser_click_xy":
|
||||
result = capCUOutput(toolCUClickXY(args))
|
||||
case "browser_type":
|
||||
result = capCUOutput(toolCUType(args))
|
||||
case "browser_key":
|
||||
result = capCUOutput(toolCUKey(args))
|
||||
case "browser_scroll":
|
||||
result = capCUOutput(toolCUScroll(args))
|
||||
case "browser_screenshot":
|
||||
result, visionImg = toolCUScreenshot()
|
||||
default:
|
||||
if isMCPTool(name) {
|
||||
result = mcpCall(name, args)
|
||||
} else {
|
||||
result = "[erreur] outil inconnu: " + name
|
||||
}
|
||||
}
|
||||
return toolExecResult{Result: result, Diff: diff, Added: diffAdd, Removed: diffDel, VisionImg: visionImg, Ask: ask}
|
||||
}
|
||||
@@ -69,6 +69,8 @@ func withInternalAPI(next http.Handler) http.Handler {
|
||||
handleInternalOAIBegin(w, r)
|
||||
case "/internal/v1/oai/end":
|
||||
handleInternalOAIEnd(w, r)
|
||||
case "/internal/v1/tool/exec":
|
||||
handleInternalToolExec(w, r)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// API interne d'exécution des outils (lot 6 de la migration Rust).
|
||||
//
|
||||
// Une boucle de chat écrite en Rust fera exécuter ses appels d'outils par ce
|
||||
// process, le temps de porter les outils eux-mêmes (lot 7) : exactement la
|
||||
// fonction qu'emploie runChatLoop (execTool), donc mêmes garde-fous (commande
|
||||
// dangereuse, écriture hors du dossier de travail, cible distante…) et mêmes
|
||||
// résultats.
|
||||
//
|
||||
// POST /internal/v1/tool/exec
|
||||
// {"name":"bash","args":{"command":"ls"},"caps":{"Agent":true,"Code":false,…}}
|
||||
// → {"label":"ls","result":"…","diff":[…],"added":N,"removed":N,"vision_img":{…},"ask":{…}}
|
||||
//
|
||||
// Le contexte de la requête porte l'arrêt : si l'appelant coupe la connexion
|
||||
// (bouton stop), la commande en cours est interrompue comme dans le Go.
|
||||
|
||||
// internalToolMaxBody : les arguments d'un appel (contenu d'un fichier écrit
|
||||
// par l'agent compris) restent très en dessous.
|
||||
const internalToolMaxBody = 32 << 20
|
||||
|
||||
func handleInternalToolExec(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("Allow", http.MethodPost)
|
||||
sendJSON(w, http.StatusMethodNotAllowed, map[string]any{"error": "POST attendu"})
|
||||
return
|
||||
}
|
||||
var in struct {
|
||||
Name string `json:"name"`
|
||||
Args map[string]any `json:"args"`
|
||||
Caps Caps `json:"caps"`
|
||||
}
|
||||
raw, err := io.ReadAll(io.LimitReader(r.Body, internalToolMaxBody+1))
|
||||
if err != nil || len(raw) > internalToolMaxBody || json.Unmarshal(raw, &in) != nil || in.Name == "" {
|
||||
sendJSON(w, http.StatusBadRequest, map[string]any{"error": "appel d'outil illisible"})
|
||||
return
|
||||
}
|
||||
if in.Args == nil {
|
||||
in.Args = map[string]any{}
|
||||
}
|
||||
label := toolCallLabel(in.Name, in.Args)
|
||||
res := execTool(r.Context(), in.Name, in.Args, label, in.Caps)
|
||||
sendJSON(w, http.StatusOK, struct {
|
||||
Label string `json:"label"`
|
||||
toolExecResult
|
||||
}{label, res})
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func appelOutil(t *testing.T, body string) map[string]any {
|
||||
t.Helper()
|
||||
w := appelOAIInterne(t, "POST", "/internal/v1/tool/exec", body)
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("code %d : %s", w.Code, w.Body.String())
|
||||
}
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return got
|
||||
}
|
||||
|
||||
// Un outil exécuté par l'API interne rend exactement ce que la boucle Go
|
||||
// aurait obtenu : même fonction (execTool), mêmes garde-fous.
|
||||
func TestOutilInterneMemesResultats(t *testing.T) {
|
||||
testHome(t)
|
||||
t.Setenv(internalTokenEnv, jetonTest)
|
||||
|
||||
got := appelOutil(t, `{"name":"outil_qui_n_existe_pas","args":{}}`)
|
||||
if got["result"] != "[erreur] outil inconnu: outil_qui_n_existe_pas" {
|
||||
t.Fatalf("outil inconnu : %v", got)
|
||||
}
|
||||
|
||||
// Commande catastrophique : refusée AVANT exécution, comme dans la boucle.
|
||||
got = appelOutil(t, `{"name":"bash","args":{"command":"rm -rf /"},"caps":{"Agent":true}}`)
|
||||
res, _ := got["result"].(string)
|
||||
if want := refusedCommandResult(dangerousCommand("rm -rf /")); res != want {
|
||||
t.Fatalf("garde-fou bash : %q, attendu %q", res, want)
|
||||
}
|
||||
if got["label"] != "rm -rf /" {
|
||||
t.Fatalf("libellé : %v", got["label"])
|
||||
}
|
||||
|
||||
// Question à l'utilisateur : l'événement remonte à l'appelant.
|
||||
got = appelOutil(t, `{"name":"ask","args":{"question":"On continue ?","options":["oui","non"]},"caps":{"Agent":true}}`)
|
||||
ask, _ := got["ask"].(map[string]any)
|
||||
if r, _ := got["result"].(string); strings.HasPrefix(r, "[erreur]") || ask["question"] != "On continue ?" {
|
||||
t.Fatalf("ask : %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutilInterneAppelIllisible(t *testing.T) {
|
||||
testHome(t)
|
||||
t.Setenv(internalTokenEnv, jetonTest)
|
||||
for _, body := range []string{"", "{", `{"args":{}}`} {
|
||||
if w := appelOAIInterne(t, "POST", "/internal/v1/tool/exec", body); w.Code != 400 {
|
||||
t.Fatalf("%q : %d", body, w.Code)
|
||||
}
|
||||
}
|
||||
if w := appelOAIInterne(t, "GET", "/internal/v1/tool/exec", ""); w.Code != 405 {
|
||||
t.Fatalf("GET : %d", w.Code)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user