Phase 7 : outils web_search et run_shell avec validation

Backend :
- tools.py : web_search (DuckDuckGo HTML sans clé, ou SearxNG via SEARX_URL) et
  run_shell (confiné au workspace, sortie bornée, timeout)
- agent.py : run_shell sensible -> émet tool_confirm et ne s'exécute pas tant que
  l'utilisateur n'a pas validé (confirm_shell)
- agent_config.py : web_search/run_shell désactivés par défaut, flag confirm_shell
- routes/shell.py : POST /api/shell/run exécute une commande validée
- routes/chat.py : relaie l'événement tool_confirm, passe confirm_shell

Frontend :
- streamChat : événement tool_confirm
- store : pendingShell + approveShell/rejectShell (réinjecte le résultat à l'agent)
- ChatPanel : carte de validation de commande (Approuver / Refuser)
- SettingsView : toggles web_search/run_shell, marqueur sensible, switch confirm_shell
- ToolCard : glyphes web_search/run_shell, statut 'à valider', aperçu d'argument

Tests : run_shell confiné, gate de confirmation (commande dangereuse non exécutée),
route shell, parser DuckDuckGo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
This commit is contained in:
MichaelandClaude Opus 4.8 committed 2026-06-29 21:09:27 +00:00
1 parent 2db6beea5c
commit de2befe31e
12 files changed
+398 -26

No files matched your search

+33
View File
@@ -9,6 +9,7 @@ import {
fileContent,
listFiles,
listSessions,
runShell,
saveConfig,
streamChat,
type AgentConfig,
@@ -42,6 +43,10 @@ interface LokiState {
refreshConfig: () => Promise<void>;
updateConfig: (patch: Partial<AgentConfig>) => Promise<void>;
pendingShell: string | null; // commande shell en attente de validation
approveShell: () => Promise<void>;
rejectShell: () => Promise<void>;
openPreview: (path: string) => Promise<void>;
setSelectedModel: (name: string) => void;
refreshStatus: () => Promise<void>;
@@ -72,6 +77,32 @@ export const useStore = create<LokiState>((set, get) => ({
previewContent: "",
config: null,
availableTools: [],
pendingShell: null,
approveShell: async () => {
const cmd = get().pendingShell;
if (!cmd) return;
set({ pendingShell: null });
let report: string;
try {
const r = await runShell(cmd);
report =
`J'ai validé la commande \`${cmd}\` (code ${r.exit_code}).\n` +
`Sortie :\n\`\`\`\n${r.output || "(vide)"}\n\`\`\``;
} catch {
report = `Échec de l'exécution de \`${cmd}\`.`;
}
await get().refreshFiles();
// On renvoie le résultat à l'agent pour qu'il poursuive.
await get().sendMessage(report);
},
rejectShell: async () => {
const cmd = get().pendingShell;
if (!cmd) return;
set({ pendingShell: null });
await get().sendMessage(`J'ai refusé la commande \`${cmd}\`. N'exécute pas cette commande.`);
},
refreshConfig: async () => {
const { config, available_tools } = await getConfig();
@@ -171,6 +202,7 @@ export const useStore = create<LokiState>((set, get) => ({
streaming: true,
streamContent: "",
streamTools: [],
pendingShell: null,
});
await streamChat(
@@ -190,6 +222,7 @@ export const useStore = create<LokiState>((set, get) => ({
}
set({ streamTools: tools });
},
onToolConfirm: (command) => set({ pendingShell: command }),
onDone: async () => {
// Repère un fichier HTML écrit pour l'afficher automatiquement.
const writtenHtml = [...get().streamTools]