Correctifs 0.8 : saisie de la cle de pilotage, identites appairees perdues a la migration

Deux defauts trouves a l'usage :

1. Impossible de saisir la cle de pilotage dans l'UI : le chargement lance une
   dizaine d'appels /api/* en parallele, chaque 401 rouvrait la modale, et
   celle-ci vide son champ a l'ouverture. Les caracteres disparaissaient au fur
   et a mesure de la frappe. Une seule demande de cle est desormais partagee par
   tous les appels concurrents. Au passage, une modale ouverte n'est plus
   ecrasee en silence (sa promesse restait pendante pour toujours).

2. migrate_07 n'importait pas .authorized_users : les identites appairees pour
   le chat chiffre. Symptome trompeur — le tunnel s'etablit, la machine
   s'affiche en ligne, mais le portail refuse toute conversation. Importe et
   couvert par un test.
This commit is contained in:
nathaninline committed 2026-08-07 20:34:43 +02:00
1 parent 98ee293a49
commit eac12112dd
5 files changed
+84 -4

No files matched your search

+22
View File
@@ -26,6 +26,7 @@ import (
"os/exec"
"path/filepath"
"runtime"
"sort"
"strings"
)
@@ -263,6 +264,27 @@ func importLegacyState(home string) error {
}
}
}
// .authorized_users : les identités appairées pour le chat chiffré de bout en
// bout, une clé publique par ligne. L'oublier coûte cher et de façon peu
// lisible : le tunnel s'établit, la machine s'affiche en ligne, mais le
// portail refuse toute conversation chiffrée — il faut ré-appairer sans
// comprendre pourquoi. Vécu sur le serveur de test.
if v := read(".authorized_users"); v != "" {
var list []string
for _, line := range strings.Split(v, "\n") {
if h := strings.ToLower(strings.TrimSpace(line)); h != "" {
list = append(list, h)
}
}
if len(list) > 0 {
sort.Strings(list)
_ = putJSON(bkState, "authorized_users", list)
fmt.Printf(" %s %d identité(s) appairée(s)\n", green("✓"), len(list))
}
}
// .pair_codes n'est PAS repris : ces codes expirent au bout de 10 minutes,
// les reprendre n'aurait aucun sens.
// mcp.json : on ne garde que la map interne, la clé « mcpServers » disparaît.
if v := read("mcp.json"); v != "" {
var f struct {
+10
View File
@@ -4,6 +4,7 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"testing"
)
@@ -40,6 +41,7 @@ func fake07Home(t *testing.T) string {
write("sysprompt.txt", "Tu es AJEAN.\n")
write("conversation.json", `{"seq":42,"messages":[{"role":"user","content":"salut"}]}`)
write("SKILLS/vieux-skill/SKILL.md", "# un skill d'avant\n")
write(".authorized_users", "AABBCCDDEEFF00112233445566778899AABBCCDDEEFF001122334455667788AA\n")
return home
}
@@ -101,6 +103,14 @@ func TestMigration07(t *testing.T) {
if conv.Seq != 42 {
t.Errorf("conversation perdue (seq %d)", conv.Seq)
}
// Les identités appairées : les perdre casse le chat chiffré du portail
// sans que rien ne le dise (le tunnel, lui, s'établit normalement).
// La liste est mise en cache une fois par process (authOnce) : en production
// c'est un process neuf qui sert après l'install, ici il faut le dire.
authOnce, authSet = sync.Once{}, map[string]bool{}
if !isAuthorizedUser("aabbccddeeff00112233445566778899aabbccddeeff001122334455667788aa") {
t.Error("identité appairée perdue : le chat E2E du portail serait refusé")
}
// Les fichiers consommés sont rangés, pas détruits.
for _, archive := range []string{"config.env", "SKILLS"} {
+26 -2
View File
@@ -1912,6 +1912,10 @@ function _openAsk(kind, message, opts){
// où cette fonction remplace le window.askConfirm du bootstrap boîte noire).
if(message && typeof message==='object'){ const o=message; opts={title:o.title, okText:o.yes, cancelText:o.no, placeholder:o.placeholder, default:o.default, danger:o.danger}; message=o.msg||''; }
opts=opts||{};
// Une modale déjà ouverte ne doit pas être écrasée en silence : son appelant
// attend une réponse, et l'écraser laissait sa promesse pendante POUR
// TOUJOURS (requête suspendue, bouton figé). On l'annule proprement d'abord.
if(_askResolver){ const prev=_askResolver; _askResolver=null; prev(_askKind==='prompt' ? null : false); }
_askKind=kind;
document.getElementById('ask-title').textContent = opts.title || (kind==='alert'?'Info':kind==='prompt'?'Saisie':'Confirmation');
document.getElementById('ask-msg').textContent = message||'';
@@ -1969,11 +1973,31 @@ async function jfetch(u, opts){
}
finally{ if(timer) clearTimeout(timer); }
if(r.status === 401){
const k = await askPrompt('Clé de pilotage ajean requise :', {title:'Authentification', placeholder:'clé…'});
if(k){ TOKEN = k.trim(); localStorage.setItem('ajean.key', TOKEN); opts.headers = authHeaders(opts.headers); r = await fetch(u, opts); }
const k = await askKeyOnce();
if(k){ opts.headers = authHeaders(opts.headers); r = await fetch(u, opts); }
}
return r;
}
// askKeyOnce : UNE seule demande de clé à la fois, partagée par tous les appels.
//
// Le chargement de l'UI lance une dizaine d'appels /api/* en parallèle, et le
// rafraîchissement périodique en relance autant. Sans ce partage, CHAQUE 401
// rouvrait la modale, laquelle vide son champ à l'ouverture : impossible de
// taper la clé, les caractères disparaissaient au fur et à mesure. Les appels
// concurrents attendent désormais la même promesse, et repartent tous avec la
// clé saisie une seule fois.
let _keyAsk = null;
function askKeyOnce(){
if(!_keyAsk){
_keyAsk = askPrompt('Clé de pilotage AJEAN requise :', {title:'Authentification', placeholder:'clé…'})
.then(k => {
_keyAsk = null;
if(k){ TOKEN = k.trim(); localStorage.setItem('ajean.key', TOKEN); }
return k;
}, e => { _keyAsk = null; throw e; });
}
return _keyAsk;
}
async function jget(u){ const r=await jfetch(u); return r.json(); }
async function jpost(u,b){ const r=await jfetch(u,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b||{})}); return r.json(); }
async function loadStatus(){
+4
View File
@@ -54,6 +54,10 @@ function _openAsk(kind, message, opts){
// où cette fonction remplace le window.askConfirm du bootstrap boîte noire).
if(message && typeof message==='object'){ const o=message; opts={title:o.title, okText:o.yes, cancelText:o.no, placeholder:o.placeholder, default:o.default, danger:o.danger}; message=o.msg||''; }
opts=opts||{};
// Une modale déjà ouverte ne doit pas être écrasée en silence : son appelant
// attend une réponse, et l'écraser laissait sa promesse pendante POUR
// TOUJOURS (requête suspendue, bouton figé). On l'annule proprement d'abord.
if(_askResolver){ const prev=_askResolver; _askResolver=null; prev(_askKind==='prompt' ? null : false); }
_askKind=kind;
document.getElementById('ask-title').textContent = opts.title || (kind==='alert'?'Info':kind==='prompt'?'Saisie':'Confirmation');
document.getElementById('ask-msg').textContent = message||'';
+22 -2
View File
@@ -30,10 +30,30 @@ async function jfetch(u, opts){
}
finally{ if(timer) clearTimeout(timer); }
if(r.status === 401){
const k = await askPrompt('Clé de pilotage ajean requise :', {title:'Authentification', placeholder:'clé…'});
if(k){ TOKEN = k.trim(); localStorage.setItem('ajean.key', TOKEN); opts.headers = authHeaders(opts.headers); r = await fetch(u, opts); }
const k = await askKeyOnce();
if(k){ opts.headers = authHeaders(opts.headers); r = await fetch(u, opts); }
}
return r;
}
// askKeyOnce : UNE seule demande de clé à la fois, partagée par tous les appels.
//
// Le chargement de l'UI lance une dizaine d'appels /api/* en parallèle, et le
// rafraîchissement périodique en relance autant. Sans ce partage, CHAQUE 401
// rouvrait la modale, laquelle vide son champ à l'ouverture : impossible de
// taper la clé, les caractères disparaissaient au fur et à mesure. Les appels
// concurrents attendent désormais la même promesse, et repartent tous avec la
// clé saisie une seule fois.
let _keyAsk = null;
function askKeyOnce(){
if(!_keyAsk){
_keyAsk = askPrompt('Clé de pilotage AJEAN requise :', {title:'Authentification', placeholder:'clé…'})
.then(k => {
_keyAsk = null;
if(k){ TOKEN = k.trim(); localStorage.setItem('ajean.key', TOKEN); }
return k;
}, e => { _keyAsk = null; throw e; });
}
return _keyAsk;
}
async function jget(u){ const r=await jfetch(u); return r.json(); }
async function jpost(u,b){ const r=await jfetch(u,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b||{})}); return r.json(); }