From 2563e4c679f4a686a8350d5ad738f0f944e38627 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 12:57:54 +0000 Subject: [PATCH] =?UTF-8?q?Plan=20d'architecture=20pour=20le=20code=20+=20?= =?UTF-8?q?garde-fou=20shell=20tol=C3=A9rant=20aux=20URLs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - make_plan(code=True) : pour une tâche d'application, produit un plan d'IMPLÉMENTATION concret (étape 1 = architecture/fichiers, puis une fonctionnalité testable à la fois), réaliste et sans dépendance externe — au lieu d'une liste d'objectifs produit (« moteur IA »…) que les petits modèles n'arrivent pas à livrer. - run_shell : le garde-fou de confinement ne bloque plus les URLs (http://localhost:8080 était rejeté à cause du « // »), et autorise la base du workspace même quand la session cible un projet (ls /workspace). Les évasions réelles (/etc, /config, ~, ../..) restent bloquées. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N --- backend/app/enhance.py | 24 ++++++++++++++++++++++-- backend/app/routes/chat.py | 3 ++- backend/app/tools.py | 8 +++++++- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/backend/app/enhance.py b/backend/app/enhance.py index 6bd9737..4290f54 100644 --- a/backend/app/enhance.py +++ b/backend/app/enhance.py @@ -22,6 +22,20 @@ _PLAN_PROMPT = ( "UNIQUEMENT les étapes, en français." ) +_PLAN_CODE_PROMPT = ( + "Tu es architecte logiciel. Établis un plan d'IMPLÉMENTATION concret pour " + "cette application, en 3 à 5 étapes courtes (une par ligne, numérotées " + "« 1. », « 2. »…).\n" + "- Étape 1 = ARCHITECTURE : quels fichiers créer et leur rôle (ex. un seul " + "index.html avec HTML+CSS+JS, ou séparer index.html / style.css / app.js).\n" + "- Étapes suivantes = construire UNE fonctionnalité concrète et testable à " + "la fois (structure/affichage, puis interactions, puis logique).\n" + "Reste RÉALISTE et réalisable en une passe : pas de dépendance externe, pas " + "de bibliothèque à installer, pas de « moteur IA » complexe si ce n'est pas " + "explicitement demandé — une logique simple en JavaScript suffit. " + "Pas d'introduction ni de conclusion, UNIQUEMENT les étapes, en français." +) + _CRITIQUE_PROMPT = ( "Tu es un relecteur exigeant. Voici une demande et la réponse d'un " "assistant. Si la réponse est correcte et complète, réponds exactement " @@ -90,13 +104,19 @@ async def make_plan( model: str, message: str, *, + code: bool = False, options: dict | None = None, keep_alive: str | None = None, ) -> list[str]: - """Renvoie la liste des étapes (vide si échec — jamais bloquant).""" + """Renvoie la liste des étapes (vide si échec — jamais bloquant). + + ``code=True`` bascule sur un plan d'ARCHITECTURE/implémentation concret + (fichiers, structure, fonctionnalités) plutôt qu'une liste d'objectifs. + """ try: raw = await _ask( - model, _PLAN_PROMPT, message[:1200], num_predict=220, + model, _PLAN_CODE_PROMPT if code else _PLAN_PROMPT, + message[:1200], num_predict=260, options=options, keep_alive=keep_alive, ) except (httpx.HTTPError, OSError) as exc: diff --git a/backend/app/routes/chat.py b/backend/app/routes/chat.py index 9d0ef73..24fe0e4 100644 --- a/backend/app/routes/chat.py +++ b/backend/app/routes/chat.py @@ -349,7 +349,8 @@ async def chat(req: ChatRequest) -> StreamingResponse: prep = asyncio.ensure_future(asyncio.gather( rag.recall(req.session_id, req.content, embed_model=cfg.get("embed_model")) if want_rag else asyncio.sleep(0, result=[]), - enhance.make_plan(model, req.content, options=run_opts, keep_alive=keep) + enhance.make_plan(model, req.content, code=use_code, + options=run_opts, keep_alive=keep) if want_plan else asyncio.sleep(0, result=[]), coder.pick_code_model(model, cfg.get("code_model")) if use_code else asyncio.sleep(0, result=model), diff --git a/backend/app/tools.py b/backend/app/tools.py index 0e16721..f68ec26 100644 --- a/backend/app/tools.py +++ b/backend/app/tools.py @@ -512,11 +512,17 @@ def _guard_shell(command: str) -> None: sortent du workspace. Combiné à la validation utilisateur, ça empêche le modèle d'écrire ailleurs que dans son workspace. """ - root = _workspace_root() + # Racine autorisée = base du workspace (pas le sous-dossier projet) : ainsi + # `ls /workspace` reste permis même quand la session cible un projet. + root = os.path.abspath(settings.workspace_dir) for token in _PATH_TOKEN.findall(command): token = token.strip() if not token: continue + # Jeton commençant par `//` = autorité d'URL (http://, ws://…) ou chemin + # réseau, jamais une évasion du système de fichiers : on l'ignore. + if token.startswith("//"): + continue if token.startswith("~"): raise ToolError( f"chemin hors du workspace refusé : {token}. "