Etat residuel en base, carte du package et docs remises d'aplomb

- 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
This commit is contained in:
nathaninline committed 2026-08-07 17:51:59 +02:00
1 parent 29bfe236cd
commit 7ecd869da0
6 files changed
+67 -84

No files matched your search

+14 -2
View File
@@ -109,9 +109,21 @@ Installation :
## Configuration ## 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` : La configuration du moteur s'édite avec `ajean edit`, qui la déroule au format `clé=valeur` dans `$EDITOR` :
+3 -1
View File
@@ -12,7 +12,9 @@ Restent des fichiers ceux qui sont faits pour être lus, édités et sauvegardé
## Six dossiers, et rien d'autre ## 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 ## Deux services qui portent enfin leur nom
+24 -11
View File
@@ -1,18 +1,31 @@
// AJEAN — cœur du binaire (package ajean, appelé par cmd/ajean). Les fichiers sont préfixés par domaine // AJEAN — cœur du binaire (package ajean, appelé par cmd/ajean). Go n'autorise
// (Go n'autorise pas de sous-dossiers dans un même package) : // 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) // 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, // 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) // llm_* client llama-server (complétions, endpoint OpenAI, bench, test)
// backend_* gestion llama.cpp : build (llamacpp), GPU, serve (ExecStart), // backend_* gestion llama.cpp : build, GPU, serve (ExecStart), modèles et
// modèles/téléchargements, catalogue, presets, config.env // téléchargements, catalogue, presets, configuration
// relay_* accès distant ajean.link : tunnel (link), chiffrement E2E, appairage // relay_* accès distant ajean.link : tunnel, chiffrement E2E, appairage
// sys_* intégration OS : install, services (systemd/launchd/Windows), // sys_* intégration OS : install, services, plateforme, process, tray,
// plateforme, process, tray, splash, tty, auto-update // splash, tty, auto-update
// //
// Les suffixes _windows/_linux/_darwin/_unix/_other portent les contraintes de // 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 package ajean
+16 -40
View File
@@ -25,8 +25,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"os" "sort"
"path/filepath"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -44,17 +43,12 @@ var (
authSet = map[string]bool{} authSet = map[string]bool{}
) )
func authUsersPath() string { return filepath.Join(AjeanHome(), ".authorized_users") }
func loadAuthUsers() { func loadAuthUsers() {
authOnce.Do(func() { authOnce.Do(func() {
b, err := os.ReadFile(authUsersPath()) var list []string
if err != nil { getJSON(bkState, "authorized_users", &list)
return for _, h := range list {
} if h = strings.ToLower(strings.TrimSpace(h)); isHexPub(h) {
for _, line := range strings.Split(string(b), "\n") {
h := strings.ToLower(strings.TrimSpace(line))
if isHexPub(h) {
authSet[h] = true authSet[h] = true
} }
} }
@@ -88,23 +82,21 @@ func authorizeUser(uPubHex string) error {
return nil return nil
} }
authSet[uPubHex] = true authSet[uPubHex] = true
_ = os.MkdirAll(AjeanHome(), 0o755) list := make([]string, 0, len(authSet))
f, err := os.OpenFile(authUsersPath(), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600) for h := range authSet {
if err != nil { list = append(list, h)
return err
} }
defer f.Close() sort.Strings(list)
_, err = f.WriteString(uPubHex + "\n") return putJSON(bkState, "authorized_users", list)
return err
} }
// ---- Codes d'appairage : à la demande, usage unique, TTL 10 min -------------- // ---- 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 // 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 // partagés avec le service d'interface (autre process) via la base — sinon les
// un fichier AjeanHome/.pair_codes — sinon les deux process ne s'accorderaient pas. // deux ne s'accorderaient pas. Stockés HACHÉS (SHA-256) : la base ne révèle
// Stockés HACHÉS (SHA-256) : le fichier ne révèle aucun code. Chaque code expire // aucun code. Chaque code expire au bout de 10 min et est consommé (retiré) au
// au bout de 10 min et est consommé (retiré) au premier appairage réussi. // premier appairage réussi.
const pairCodeTTL = 10 * time.Minute const pairCodeTTL = 10 * time.Minute
@@ -113,8 +105,6 @@ type pairEntry struct {
Exp int64 `json:"e"` // expiration (unix ms) Exp int64 `json:"e"` // expiration (unix ms)
} }
func pairCodesPath() string { return filepath.Join(AjeanHome(), ".pair_codes") }
func hashPairCode(code string) string { func hashPairCode(code string) string {
h := sha256.Sum256([]byte(strings.ToUpper(strings.TrimSpace(code)))) h := sha256.Sum256([]byte(strings.ToUpper(strings.TrimSpace(code))))
return hex.EncodeToString(h[:]) 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. // loadPairEntries lit le store et purge au passage les entrées expirées.
func loadPairEntries() []pairEntry { func loadPairEntries() []pairEntry {
b, err := os.ReadFile(pairCodesPath())
if err != nil {
return nil
}
var es []pairEntry var es []pairEntry
if json.Unmarshal(b, &es) != nil { if !getJSON(bkState, "pair_codes", &es) {
return nil return nil
} }
now := time.Now().UnixMilli() now := time.Now().UnixMilli()
@@ -140,17 +126,7 @@ func loadPairEntries() []pairEntry {
return kept return kept
} }
func savePairEntries(es []pairEntry) error { func savePairEntries(es []pairEntry) error { return putJSON(bkState, "pair_codes", es) }
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
}
// newPairCode génère un code frais (usage unique, 10 min), le persiste haché et // newPairCode génère un code frais (usage unique, 10 min), le persiste haché et
// le retourne en clair. // le retourne en clair.
+6 -8
View File
@@ -19,8 +19,10 @@ import (
"time" "time"
) )
func lcJobStatePath() string { return filepath.Join(AjeanHome(), ".lc_job.json") } // L'ENTÊTE du job vit en base ; ses LIGNES restent un fichier, écrit en append.
func lcJobLogPath() string { return filepath.Join(AjeanHome(), ".lc_job.log") } // 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 // lcPersisted est la forme sur disque : l'entête du job, sans les lignes (elles
// vivent dans le .log à côté, en append). // vivent dans le .log à côté, en append).
@@ -60,7 +62,7 @@ func lcSave(force bool) {
OldCommit: lcCur.OldCommit, NewCommit: lcCur.NewCommit, OldCommit: lcCur.OldCommit, NewCommit: lcCur.NewCommit,
}) })
if err == nil { 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 // 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. // plutôt que de le faire passer pour vivant ou de l'effacer en silence.
func lcRestore() { func lcRestore() {
b, err := os.ReadFile(lcJobStatePath())
if err != nil {
return
}
var p lcPersisted var p lcPersisted
if json.Unmarshal(b, &p) != nil || p.Action == "" { if !getJSON(bkState, "build_job", &p) || p.Action == "" {
return return
} }
j := &lcJob{ j := &lcJob{
+4 -22
View File
@@ -1,6 +1,6 @@
// web_sysprompt.go — prompt système personnalisé de l'utilisateur, persisté // web_sysprompt.go — prompt système personnalisé de l'utilisateur, persisté
// CÔTÉ SERVEUR ($AJEAN_HOME/sysprompt.txt) et partagé entre appareils, comme la // CÔTÉ SERVEUR (en base) et partagé entre appareils, comme la conversation
// conversation elle-même. Historique : avant la conversation serveur (v0.4.x), // 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, // 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 // /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 // effet. Il est maintenant lu ici par la génération (chat_conversation.go), et
@@ -10,32 +10,14 @@ package ajean
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"os"
"path/filepath"
"strings" "strings"
) )
func sysPromptPath() string { return filepath.Join(AjeanHome(), "sysprompt.txt") }
// readSysPrompt renvoie le prompt système personnalisé ("" si absent). // readSysPrompt renvoie le prompt système personnalisé ("" si absent).
func readSysPrompt() string { func readSysPrompt() string { return getStr(bkState, "sysprompt") }
b, err := os.ReadFile(sysPromptPath())
if err != nil {
return ""
}
return strings.TrimSpace(string(b))
}
func saveSysPrompt(text string) error { func saveSysPrompt(text string) error {
text = strings.TrimSpace(text) return putStr(bkState, "sysprompt", 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)
} }
// handleSysPrompt : // handleSysPrompt :