From 7ecd869da0e9a6b2d6fda4ff8a4f58b32317722b Mon Sep 17 00:00:00 2001 From: nathaninline Date: Fri, 7 Aug 2026 17:51:59 +0200 Subject: [PATCH] Etat residuel en base, carte du package et docs remises d'aplomb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sysprompt, utilisateurs appairés, codes d'appairage et etat du job de build passent en base ; ne restent des fichiers que la cle E2E, les certificats et les journaux (le log de build reste un fichier : des milliers de lignes) - doc.go remis a jour (run.go, store.go, les deux services, tools/assemble-ui) - README et notes de release ne pretendent plus que la racine ne contient que six dossiers : c'etait faux --- README.md | 16 +++++++-- RELEASE_NOTES.md | 4 ++- internal/ajean/doc.go | 35 +++++++++++++------ internal/ajean/relay_e2eauth.go | 56 +++++++++--------------------- internal/ajean/web_llamacpp_job.go | 14 ++++---- internal/ajean/web_sysprompt.go | 26 +++----------- 6 files changed, 67 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 0437fec..ba2a079 100644 --- a/README.md +++ b/README.md @@ -109,9 +109,21 @@ Installation : ## Configuration -Tout vit sous **`$AJEAN_HOME`** (`/etc/ajean` sous Linux/macOS, `%ProgramData%\ajean` sous Windows), qui contient exactement six dossiers — `backends/`, `bin/`, `presets/`, `memory/`, `models/`, `workspace/` — et la base `ajean.db`. +Tout vit sous **`$AJEAN_HOME`** (`/etc/ajean` sous Linux/macOS, `%ProgramData%\ajean` sous Windows) : -Cette base, un unique fichier [bbolt](https://github.com/etcd-io/bbolt), remplace la dizaine de fichiers d'état d'autrefois : configuration, préférences de l'interface, conversation, clés et interrupteurs y vivent désormais ensemble. Restent des fichiers ce qui se lit et s'édite à la main : les presets (`presets/*.env`), les pages de mémoire (`memory/*.md`) et, bien sûr, les modèles. +| | | +|---|---| +| `backends/` | llama.cpp, compilé ou téléchargé | +| `bin/` | le binaire installé (Windows) | +| `models/` | les `.gguf` | +| `presets/` | un `.env` par preset | +| `memory/` | les pages de mémoire de l'IA (`.md`) | +| `workspace/` | ce que l'IA écrit en mode agent | +| `ajean.db` | tout l'état : configuration, préférences, conversation, clés, interrupteurs | + +S'y ajoutent à la racine les quelques fichiers qui ne peuvent pas aller ailleurs : `.e2e_key` (clé privée du chiffrement de bout en bout), `certs/` (certificats TLS gérés par certmagic), et les journaux et fichiers PID des services. + +La base, un unique fichier [bbolt](https://github.com/etcd-io/bbolt), remplace la dizaine de fichiers d'état d'autrefois. Restent des fichiers ce qui se lit et s'édite à la main : les presets, les pages de mémoire et, bien sûr, les modèles. La configuration du moteur s'édite avec `ajean edit`, qui la déroule au format `clé=valeur` dans `$EDITOR` : diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index d69c6c0..d4cd118 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -12,7 +12,9 @@ Restent des fichiers ceux qui sont faits pour être lus, édités et sauvegardé ## Six dossiers, et rien d'autre -`$AJEAN_HOME` contient exactement `backends/`, `bin/`, `presets/`, `memory/`, `models/`, `workspace/`, plus la base. `configs/` devient `presets/`, `MEMORY/` devient `memory/`, et les `.gguf` ont enfin leur `models/` au lieu d'être posés à la racine. +`$AJEAN_HOME` contient `backends/`, `bin/`, `presets/`, `memory/`, `models/`, `workspace/`, plus la base. `configs/` devient `presets/`, `MEMORY/` devient `memory/`, et les `.gguf` ont enfin leur `models/` au lieu d'être posés à la racine. + +Ne restent à la racine que ce qui ne peut pas aller ailleurs : la clé privée du chiffrement de bout en bout, le dossier des certificats TLS, et les journaux et fichiers PID des services. ## Deux services qui portent enfin leur nom diff --git a/internal/ajean/doc.go b/internal/ajean/doc.go index f595dba..d6128d2 100644 --- a/internal/ajean/doc.go +++ b/internal/ajean/doc.go @@ -1,18 +1,31 @@ -// AJEAN — cœur du binaire (package ajean, appelé par cmd/ajean). Les fichiers sont préfixés par domaine -// (Go n'autorise pas de sous-dossiers dans un même package) : +// AJEAN — cœur du binaire (package ajean, appelé par cmd/ajean). Go n'autorise +// pas de sous-dossiers dans un même package : les fichiers sont donc préfixés +// par domaine. // -// main.go point d'entrée : dispatch des sous-commandes, chemins (AJEANHome…) +// run.go point d'entrée : dispatch des sous-commandes, AjeanHome() et +// l'arborescence de données +// store.go LA base (bbolt) : configuration, préférences, conversation, +// clés, jetons, interrupteurs — tout l'état non éditable à la main // cli_* expérience « application » (double-clic : UI + tray + splash) -// web_* serveur HTTP local :8090 (UI embarquée via go:embed ui/, auth, prefs) +// web_* serveur HTTP :8090 (UI embarquée via go:embed ui/, auth, prefs) // chat_* chat CLI + conversation serveur partagée, compaction, mémoire, -// outils de l'agent (dont accès internet via Crawl4AI) +// outils de l'agent (dont accès internet) // llm_* client llama-server (complétions, endpoint OpenAI, bench, test) -// backend_* gestion llama.cpp : build (llamacpp), GPU, serve (ExecStart), -// modèles/téléchargements, catalogue, presets, config.env -// relay_* accès distant ajean.link : tunnel (link), chiffrement E2E, appairage -// sys_* intégration OS : install, services (systemd/launchd/Windows), -// plateforme, process, tray, splash, tty, auto-update +// backend_* gestion llama.cpp : build, GPU, serve (ExecStart), modèles et +// téléchargements, catalogue, presets, configuration +// relay_* accès distant ajean.link : tunnel, chiffrement E2E, appairage +// sys_* intégration OS : install, services, plateforme, process, tray, +// splash, tty, auto-update // // Les suffixes _windows/_linux/_darwin/_unix/_other portent les contraintes de -// compilation par OS. ui/index.html est GÉNÉRÉ depuis ui/src/ (voir ui/assemble.ps1). +// compilation par OS. +// +// DEUX SERVICES à l'exécution, un seul binaire : +// - ajean-engine (« ajean serve ») exec llama-server ; +// - ajean-ui (« ajean web ») sert l'UI locale, le tunnel du relais et +// l'endpoint OpenAI depuis un SEUL process — condition d'une conversation +// unique, celle-ci vivant en mémoire (voir chat_conversation.go). +// +// ui/index.html est GÉNÉRÉ depuis ui/src/ : éditer les sources puis +// `go generate ./internal/ajean` (voir tools/assemble-ui). package ajean diff --git a/internal/ajean/relay_e2eauth.go b/internal/ajean/relay_e2eauth.go index 8b372b8..d56a460 100644 --- a/internal/ajean/relay_e2eauth.go +++ b/internal/ajean/relay_e2eauth.go @@ -25,8 +25,7 @@ import ( "encoding/json" "fmt" "net/http" - "os" - "path/filepath" + "sort" "strings" "sync" "time" @@ -44,17 +43,12 @@ var ( authSet = map[string]bool{} ) -func authUsersPath() string { return filepath.Join(AjeanHome(), ".authorized_users") } - func loadAuthUsers() { authOnce.Do(func() { - b, err := os.ReadFile(authUsersPath()) - if err != nil { - return - } - for _, line := range strings.Split(string(b), "\n") { - h := strings.ToLower(strings.TrimSpace(line)) - if isHexPub(h) { + var list []string + getJSON(bkState, "authorized_users", &list) + for _, h := range list { + if h = strings.ToLower(strings.TrimSpace(h)); isHexPub(h) { authSet[h] = true } } @@ -88,23 +82,21 @@ func authorizeUser(uPubHex string) error { return nil } authSet[uPubHex] = true - _ = os.MkdirAll(AjeanHome(), 0o755) - f, err := os.OpenFile(authUsersPath(), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600) - if err != nil { - return err + list := make([]string, 0, len(authSet)) + for h := range authSet { + list = append(list, h) } - defer f.Close() - _, err = f.WriteString(uPubHex + "\n") - return err + sort.Strings(list) + return putJSON(bkState, "authorized_users", list) } // ---- Codes d'appairage : à la demande, usage unique, TTL 10 min -------------- // // Générés par « ajean link code » (ou affichés par « ajean link »), ils sont -// partagés avec le worker « ajean link --foreground » (systemd, autre process) via -// un fichier AjeanHome/.pair_codes — sinon les deux process ne s'accorderaient pas. -// Stockés HACHÉS (SHA-256) : le fichier ne révèle aucun code. Chaque code expire -// au bout de 10 min et est consommé (retiré) au premier appairage réussi. +// partagés avec le service d'interface (autre process) via la base — sinon les +// deux ne s'accorderaient pas. Stockés HACHÉS (SHA-256) : la base ne révèle +// aucun code. Chaque code expire au bout de 10 min et est consommé (retiré) au +// premier appairage réussi. const pairCodeTTL = 10 * time.Minute @@ -113,8 +105,6 @@ type pairEntry struct { Exp int64 `json:"e"` // expiration (unix ms) } -func pairCodesPath() string { return filepath.Join(AjeanHome(), ".pair_codes") } - func hashPairCode(code string) string { h := sha256.Sum256([]byte(strings.ToUpper(strings.TrimSpace(code)))) return hex.EncodeToString(h[:]) @@ -122,12 +112,8 @@ func hashPairCode(code string) string { // loadPairEntries lit le store et purge au passage les entrées expirées. func loadPairEntries() []pairEntry { - b, err := os.ReadFile(pairCodesPath()) - if err != nil { - return nil - } var es []pairEntry - if json.Unmarshal(b, &es) != nil { + if !getJSON(bkState, "pair_codes", &es) { return nil } now := time.Now().UnixMilli() @@ -140,17 +126,7 @@ func loadPairEntries() []pairEntry { return kept } -func savePairEntries(es []pairEntry) error { - if err := os.MkdirAll(AjeanHome(), 0o755); err != nil { - return err - } - b, _ := json.Marshal(es) - tmp := pairCodesPath() + ".tmp" - if err := os.WriteFile(tmp, b, 0o600); err != nil { - return err - } - return os.Rename(tmp, pairCodesPath()) // remplacement atomique -} +func savePairEntries(es []pairEntry) error { return putJSON(bkState, "pair_codes", es) } // newPairCode génère un code frais (usage unique, 10 min), le persiste haché et // le retourne en clair. diff --git a/internal/ajean/web_llamacpp_job.go b/internal/ajean/web_llamacpp_job.go index cd66b81..61c651e 100644 --- a/internal/ajean/web_llamacpp_job.go +++ b/internal/ajean/web_llamacpp_job.go @@ -19,8 +19,10 @@ import ( "time" ) -func lcJobStatePath() string { return filepath.Join(AjeanHome(), ".lc_job.json") } -func lcJobLogPath() string { return filepath.Join(AjeanHome(), ".lc_job.log") } +// L'ENTÊTE du job vit en base ; ses LIGNES restent un fichier, écrit en append. +// Une compilation en produit des milliers : les faire transiter par une +// transaction chacune coûterait un fsync par ligne. +func lcJobLogPath() string { return filepath.Join(AjeanHome(), "ajean-build.log") } // lcPersisted est la forme sur disque : l'entête du job, sans les lignes (elles // vivent dans le .log à côté, en append). @@ -60,7 +62,7 @@ func lcSave(force bool) { OldCommit: lcCur.OldCommit, NewCommit: lcCur.NewCommit, }) if err == nil { - _ = os.WriteFile(lcJobStatePath(), b, 0o644) + _ = putBytes(bkState, "build_job", b) } } @@ -82,12 +84,8 @@ func lcResetLog() { _ = os.Remove(lcJobLogPath()) } // compilation est morte avec le process précédent, on le marque interrompu // plutôt que de le faire passer pour vivant ou de l'effacer en silence. func lcRestore() { - b, err := os.ReadFile(lcJobStatePath()) - if err != nil { - return - } var p lcPersisted - if json.Unmarshal(b, &p) != nil || p.Action == "" { + if !getJSON(bkState, "build_job", &p) || p.Action == "" { return } j := &lcJob{ diff --git a/internal/ajean/web_sysprompt.go b/internal/ajean/web_sysprompt.go index 06c461b..052fbed 100644 --- a/internal/ajean/web_sysprompt.go +++ b/internal/ajean/web_sysprompt.go @@ -1,6 +1,6 @@ // web_sysprompt.go — prompt système personnalisé de l'utilisateur, persisté -// CÔTÉ SERVEUR ($AJEAN_HOME/sysprompt.txt) et partagé entre appareils, comme la -// conversation elle-même. Historique : avant la conversation serveur (v0.4.x), +// CÔTÉ SERVEUR (en base) et partagé entre appareils, comme la conversation +// elle-même. Historique : avant la conversation serveur (v0.4.x), // l'UI envoyait son prompt système dans chaque requête /api/chat ; depuis, // /api/chat/send ne porte que le message → le champ de l'UI n'avait plus aucun // effet. Il est maintenant lu ici par la génération (chat_conversation.go), et @@ -10,32 +10,14 @@ package ajean import ( "encoding/json" "net/http" - "os" - "path/filepath" "strings" ) -func sysPromptPath() string { return filepath.Join(AjeanHome(), "sysprompt.txt") } - // readSysPrompt renvoie le prompt système personnalisé ("" si absent). -func readSysPrompt() string { - b, err := os.ReadFile(sysPromptPath()) - if err != nil { - return "" - } - return strings.TrimSpace(string(b)) -} +func readSysPrompt() string { return getStr(bkState, "sysprompt") } func saveSysPrompt(text string) error { - text = strings.TrimSpace(text) - if text == "" { - err := os.Remove(sysPromptPath()) - if os.IsNotExist(err) { - return nil - } - return err - } - return os.WriteFile(sysPromptPath(), []byte(text+"\n"), 0o644) + return putStr(bkState, "sysprompt", strings.TrimSpace(text)) } // handleSysPrompt :