v0.2.2 : mise a jour de securite — boite noire E2E & relais aveugle

- Chiffrement E2E du chat navigateur<->agent (e2e.go) : le relais ne voit
  que de l'opaque, ancre par empreinte X25519 confirmee au `jean link`.
- `jean link` refuse desormais tout chat en clair via le tunnel (403) ;
  seul /api/e2e/chat porte du contenu. Endpoint OpenAI clair desactive par
  defaut (JEAN_LINK_ALLOW_OAI=1 pour le reactiver).
- Le portail web n'est plus servi par le relais : code livre par une origine
  hors de son controle, le relais devient un pur tube aveugle.
This commit is contained in:
nathaninline committed 2026-06-23 19:45:17 +02:00
1 parent ca5d3f9197
commit a8a016c1b8
11 files changed
+403 -62

No files matched your search

+49 -29
View File
@@ -2,6 +2,7 @@ package main
import (
"bufio"
"context"
"embed"
"encoding/json"
"fmt"
@@ -95,6 +96,7 @@ func newWebMux() *http.ServeMux {
api("/api/bench", handleBench)
api("/api/bench/last", handleBenchLast)
api("/api/chat", handleChat)
api("/api/e2e/chat", handleE2EChat) // chat chiffré E2E (boîte noire via le relais)
return mux
}
@@ -566,18 +568,57 @@ func handleBenchLast(w http.ResponseWriter, r *http.Request) {
sendJSON(w, 200, map[string]any{"ok": true, "result": sb.Result, "model": sb.Model, "at": sb.At})
}
func handleChat(w http.ResponseWriter, r *http.Request) {
var body struct {
Messages []Message `json:"messages"`
Temperature float64 `json:"temperature"`
// chatReq est le corps d'une requête de chat (commun au chat clair et au chat E2E).
type chatReq struct {
Messages []Message `json:"messages"`
Temperature float64 `json:"temperature"`
// Optional per-request capability overrides (used by ajean.link agents,
// which carry their own tools/skills toggles). nil = inherit the
// machine's global config.
Tools *bool `json:"tools"`
Skills *bool `json:"skills"`
}
// runChatStream exécute le chat et pousse chaque événement (delta) via emit, qui
// renvoie false pour interrompre. Partagé par handleChat et handleE2EChat.
func runChatStream(ctx context.Context, body chatReq, emit func(map[string]any) bool) {
if body.Temperature == 0 {
body.Temperature = 0.7
}
caps := globalCaps()
if body.Tools != nil {
caps.Tools = *body.Tools
}
if body.Skills != nil {
caps.Skills = *body.Skills
}
msgs := InjectSkills(body.Messages, caps)
_ = runChat(ctx, msgs, body.Temperature, caps, func(ev StreamEvent) bool {
if ev.Err != nil {
return emit(map[string]any{"error": ev.Err.Error()})
}
if ev.ToolUsed != nil {
return emit(map[string]any{"tool_used": map[string]any{"name": ev.ToolUsed.Name, "label": ev.ToolUsed.Label, "result": ev.ToolUsed.Result, "done": ev.ToolUsed.Done, "typing": ev.ToolUsed.Typing}})
}
if ev.Stats != nil {
return emit(map[string]any{"stats": ev.Stats})
}
if ev.Reasoning != "" {
return emit(map[string]any{"reasoning_content": ev.Reasoning})
}
if ev.Content != "" {
return emit(map[string]any{"content": ev.Content})
}
return true
})
}
func handleChat(w http.ResponseWriter, r *http.Request) {
var body chatReq
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, err.Error(), 400)
return
}
if body.Temperature == 0 {
body.Temperature = 0.7
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("X-Accel-Buffering", "no")
@@ -592,27 +633,6 @@ func handleChat(w http.ResponseWriter, r *http.Request) {
}
return true
}
msgs := InjectSkills(body.Messages)
_ = runChat(msgs, body.Temperature, func(ev StreamEvent) bool {
if ev.Err != nil {
emit(map[string]any{"error": ev.Err.Error()})
return true
}
if ev.ToolUsed != nil {
emit(map[string]any{"tool_used": map[string]any{"name": ev.ToolUsed.Name, "label": ev.ToolUsed.Label, "result": ev.ToolUsed.Result, "done": ev.ToolUsed.Done, "typing": ev.ToolUsed.Typing}})
return true
}
if ev.Stats != nil {
emit(map[string]any{"stats": ev.Stats})
return true
}
if ev.Reasoning != "" {
return emit(map[string]any{"reasoning_content": ev.Reasoning})
}
if ev.Content != "" {
return emit(map[string]any{"content": ev.Content})
}
return true
})
runChatStream(r.Context(), body, emit)
}