From 0430aa2281d63cbc89f8da61e0d353bb54008a6a Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Wed, 19 Aug 2026 08:08:41 +0200 Subject: [PATCH] =?UTF-8?q?Mode=20code=20:=20la=20capture=20d'=C3=A9cran?= =?UTF-8?q?=20sans=20acc=C3=A8s=20internet,=20born=C3=A9e=20au=20local?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le modèle cherchait à vérifier visuellement son travail, ne trouvait pas web_screenshot (offert seulement avec l'accès internet), en concluait « pas de Chromium local » et partait installer puppeteer — alors qu'un Chromium headless est dans l'image depuis toujours. L'outil est donc offert aussi au mode code sans internet, mais borné aux adresses locales (localhost, 127.0.0.1, ::1, *.localhost) : photographier son propre serveur de dev est une vérification locale, pas une sortie sur le web — l'interrupteur d'accès internet garde tout son sens. Le prompt du builder dit explicitement que le navigateur est déjà là et qu'il ne faut installer aucun paquet de navigateur. Co-Authored-By: Claude Opus 5 --- internal/loki/agents/builder.md | 2 +- internal/loki/chat_screenshot.go | 26 +++++++++++++++++++++++++- internal/loki/llm_client.go | 18 +++++++++++------- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/internal/loki/agents/builder.md b/internal/loki/agents/builder.md index caea3e7..4af611b 100644 --- a/internal/loki/agents/builder.md +++ b/internal/loki/agents/builder.md @@ -4,7 +4,7 @@ Method — in this order: 1. If the task has no acceptance criteria yet, set them FIRST with the criteria tool (action=add): 2-6 short, testable statements of what DONE means. For a trivial task (one obvious change), skip criteria and just do it. 2. Explore before you change: read the relevant files (read, grep, glob, git_status). Never edit a file you have not read. 3. Implement with edit (small patches) or write (new files). Match the style of the surrounding code. -4. Prove it: run the build/tests with bash. A change that was never run is not done. +4. Prove it: run the build/tests with bash. A change that was never run is not done. For a web page or UI, start the dev server with bash_bg then use web_screenshot on its localhost URL — a headless Chromium is ALREADY installed, so NEVER install puppeteer, playwright or a browser. 5. Report briefly what changed and how you verified it. Rules: diff --git a/internal/loki/chat_screenshot.go b/internal/loki/chat_screenshot.go index 5e13291..f596145 100644 --- a/internal/loki/chat_screenshot.go +++ b/internal/loki/chat_screenshot.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "net/http" + neturl "net/url" "os" "os/exec" "path/filepath" @@ -242,7 +243,7 @@ func safeSlug(s string) string { return s } -func toolWebScreenshot(args map[string]any) string { +func toolWebScreenshot(args map[string]any, caps Caps) string { bin := playwrightBin() if bin == "" { return "[erreur] Playwright n'est pas installé dans cette image (bâtie avec PLAYWRIGHT=0)." @@ -255,6 +256,13 @@ func toolWebScreenshot(args map[string]any) string { if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") { return "[erreur] url invalide : elle doit commencer par http:// ou https://" } + // Accès internet coupé : la capture reste offerte au MODE CODE, mais bornée + // aux adresses locales — voir sa propre page de dev, pas le web. Sans cette + // borne, l'outil rouvrirait par la fenêtre ce que l'interrupteur ferme. + if !caps.Internet && !isLocalURL(url) { + return "[refusé] L'accès internet est coupé : la capture ne marche que sur une adresse locale " + + "(localhost / 127.0.0.1), par exemple ton serveur de dev. Active l'accès internet pour le web." + } // width n'est plus déclaré dans le schéma (budget de préambule), mais reste // honoré s'il arrive quand même : un modèle qui l'invente obtient le // comportement attendu plutôt qu'un paramètre ignoré en silence. @@ -397,3 +405,19 @@ func lastLines(s string, n int) string { } return strings.Join(keep, " · ") } + +// isLocalURL : l'adresse désigne-t-elle cette machine ? Sert de borne quand +// l'accès internet est coupé mais que le mode code doit pouvoir photographier +// son propre serveur de dev. +func isLocalURL(raw string) bool { + u, err := neturl.Parse(raw) + if err != nil { + return false + } + host := u.Hostname() + switch host { + case "localhost", "127.0.0.1", "::1", "0.0.0.0": + return true + } + return strings.HasSuffix(host, ".localhost") +} diff --git a/internal/loki/llm_client.go b/internal/loki/llm_client.go index e9546f3..38b99e3 100644 --- a/internal/loki/llm_client.go +++ b/internal/loki/llm_client.go @@ -296,12 +296,16 @@ func EnabledTools(caps Caps) []Tool { // donc prompt et outils restent cohérents — pas de web_search halluciné. if caps.Agent && caps.Internet { tools = append(tools, webSearchTool(), webOpenTool(), webReadTool(), webGrepTool()) - // Capture d'écran : seulement si Playwright est réellement présent dans - // l'image. Annoncer un outil absent enverrait le modèle en boucle de - // réessai sur un échec systématique. - if screenshotAvailable() { - tools = append(tools, webScreenshotTool()) - } + } + // Capture d'écran : seulement si Playwright est réellement présent dans + // l'image (annoncer un outil absent enverrait le modèle en boucle de + // réessai). Offerte aussi SANS accès internet quand le mode code est actif : + // photographier son propre serveur de dev est une vérification locale, pas + // une sortie sur le web — et sans elle, le modèle croyait n'avoir aucun + // navigateur et perdait des minutes à installer puppeteer. La borne aux + // adresses locales vit dans toolWebScreenshot. + if caps.Agent && (caps.Internet || caps.Code) && screenshotAvailable() { + tools = append(tools, webScreenshotTool()) } // Outils MCP : serveurs tiers configurés par le propriétaire de la machine. // Comme bash, ils exécutent du code arbitraire côté hôte → réservés au mode @@ -1195,7 +1199,7 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps case "web_grep": result = capWebOutput(toolWebGrep(args)) case "web_screenshot": - result = toolWebScreenshot(args) + result = toolWebScreenshot(args, caps) default: if isMCPTool(tc.Function.Name) { result = mcpCall(tc.Function.Name, args)