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

+20 -5
View File
@@ -16,8 +16,17 @@ DEFAULT_SYSTEM_PROMPT = (
"concise en français. Après avoir écrit un fichier, propose un aperçu."
)
# Outils réellement disponibles (les sensibles arrivent en phase 7).
AVAILABLE_TOOLS = ["read_file", "write_file", "list_dir"]
# Outils disponibles. Les sensibles (web_search, run_shell) sont désactivés
# par défaut, conformément à la maquette.
AVAILABLE_TOOLS = ["read_file", "write_file", "list_dir", "web_search", "run_shell"]
SENSITIVE_TOOLS = {"run_shell"}
DEFAULT_TOOL_STATE = {
"read_file": True,
"write_file": True,
"list_dir": True,
"web_search": False,
"run_shell": False,
}
DEFAULT_CONFIG: dict = {
"system_prompt": DEFAULT_SYSTEM_PROMPT,
@@ -25,7 +34,9 @@ DEFAULT_CONFIG: dict = {
"top_p": 0.9,
"top_k": 40,
"max_tokens": 2048,
"tools": {name: True for name in AVAILABLE_TOOLS},
"tools": dict(DEFAULT_TOOL_STATE),
# Demander une validation utilisateur avant toute commande shell.
"confirm_shell": True,
}
@@ -34,7 +45,7 @@ def get_config() -> dict:
stored = db.get_config_value(CONFIG_KEY) or {}
cfg = {**DEFAULT_CONFIG, **stored}
cfg["tools"] = {
name: bool(stored.get("tools", {}).get(name, True))
name: bool(stored.get("tools", {}).get(name, DEFAULT_TOOL_STATE[name]))
for name in AVAILABLE_TOOLS
}
return cfg
@@ -45,7 +56,11 @@ def save_config(patch: dict) -> dict:
cfg = {**get_config(), **{k: v for k, v in patch.items() if v is not None}}
if "tools" in patch and patch["tools"]:
cfg["tools"] = {
name: bool(patch["tools"].get(name, cfg["tools"].get(name, True)))
name: bool(
patch["tools"].get(
name, cfg["tools"].get(name, DEFAULT_TOOL_STATE[name])
)
)
for name in AVAILABLE_TOOLS
}
db.set_config_value(CONFIG_KEY, cfg)