# Installeur Lyra
Point d'entree unique : `./installer/install.sh` (depuis un clone du repo).
Deux frontaux consomment exactement le meme pipeline :
- `--tui` (defaut) : installeur terminal Rich (boot ASCII, menu MCPs a
cocher aux fleches, mascotte, pipeline anime). Apres le boot, un ecran
propose de rester en terminal ou de basculer vers l'app graphique
(le processus devient alors le serveur de l'app).
- `--app` : lance directement l'installeur graphique local (design
neutroncore) sur `http://127.0.0.1:9877/ui/` — backend Python stdlib,
frontend React pre-builde et commite dans `app/backend/static/`.
- `--headless` : sans interface, chaque question recoit sa valeur par defaut
(journalisee), sortie texte ; MCP par `--mcps id1,id2`. Pour les tests en VM
(`tests/installer/vm_install_test.sh`) et les machines sans terminal.
`--demo` simule tout le parcours sans executer une seule commande.
## Pipeline
Ordre reel des etapes (`installer/core/pipeline.py:build_pipeline`), verifie
en conditions reelles sur Fedora/Ubuntu/Arch. `post` (reindexation RAG)
tourne **avant** `daemon` : le demon charge lui aussi ChromaDB au demarrage,
et les deux en concurrence sur un repertoire tout neuf provoquaient une
course (schema sqlite cree deux fois) -- observee et corrigee.
```mermaid
flowchart LR
SYS["system
paquets distro"] --> CLONE["clone
repo Lyra (SSH -> PAT si besoin)"]
CLONE --> VENV["venv
python -m venv"]
VENV --> PIP["pip
chromadb, torch cpu,
sentence-transformers..."]
PIP --> PIPER["piper
lien /usr/local/bin/piper"]
PIPER --> VOICE["voice
voix fr_FR-upmc-medium"]
VOICE --> OLLAMA["ollama
client (meme en mode distant)"]
OLLAMA --> MODELS["models
pull qwen2.5-coder:0.5b
+ llama3.2:1b"]
MODELS --> MCPS["MCP selectionnes
(0 a N, clone + install)"]
MCPS --> CONFIG["config
config.yaml / secrets.yaml"]
CONFIG --> POST["post
reindexation RAG (ChromaDB)"]
POST --> DAEMON["daemon
lyra-daemon (systemd user)"]
```
Les repos MCP (`fedora-agents`, `halo-light`, `pylips-mcp`...) partagent
le meme fallback d'authentification que le clone du repo principal
(`core/gitauth.py resolve_repo_url`) : SSH testee en premier (et le succes
alimente `known_hosts`), sinon un Personal Access Token est demande **une
seule fois par run** et reutilise pour tous les MCPs prives suivants --
jamais ecrit sur disque.
## Architecture
```
installer/
├── install.sh # bootstrap (venv ephemere rich/pyyaml/requests) + dispatch
├── assets/mascots.json # mascottes ASCII partagees TUI/app (source: neutroncore)
├── core/ # logique pure, testee (tests/installer/)
│ ├── catalog.yaml # catalogue declaratif des MCPs <-- AJOUTER UN MCP ICI
│ ├── catalog.py # chargement + validation stricte
│ ├── osdetect.py # /etc/os-release -> famille + paquets (fedora/debian/arch)
│ ├── state.py # InstallState immuable
│ ├── events.py # Output/Progress/StepChange/Ask/Result + AskBroker
│ ├── runner.py # Popen streamable, jamais shell=True
│ ├── pipeline.py # StepDef declaratifs + run_pipeline
│ ├── configpatch.py # config.yaml/secrets.yaml en YAML in-process
│ └── steps/ # system, clone, venv, piper, ollama, mcps,
│ # config, systemd (demon!), post
├── tui/ # frontal terminal
└── app/ # frontal web (backend/ stdlib 9877 + frontend/ Vite)
```
## Ajouter un MCP au catalogue
Une entree YAML dans `core/catalog.yaml` suffit : id, repo (prive
amineutron/...), dest, runtime (python|node), fields (les champs `secret:
true` vont dans secrets.yaml chmod 600, jamais dans config.yaml),
config/server (blocs injectes dans config.yaml), check (http|tcp),
extra_steps (npm_build, sudoers, hue_pairing, pip_catt). `sudoers` copie
`/scripts` en root:root dans /usr/local/lib/lyra/scripts puis ecrit
/etc/sudoers.d/lyra (une regle par script, visudo -cf, jamais de glob). `hue_pairing`
apparie le bridge en https (le http est redirige) et ecrit `~/.config/halo-light/config.json`
(0600) pour le serveur halo-light, en plus de secrets.yaml. Les deux
frontaux et les tests le prennent en compte automatiquement.
## Garanties
- Les secrets (credentials TV, pairing Hue username+clientkey) ne
touchent jamais config.yaml (`assert_no_secrets`, teste).
- config.yaml/secrets.yaml existants : backup horodate avant ecriture.
- Le demon systemd `lyra-daemon` est installe et active (PATH avec
`.venv/bin` en tete — piege documente dans CLAUDE.md).
- Repos prives : pre-vol SSH GitHub, fallback https+PAT saisi a la volee
(jamais ecrit sur disque).
## Derrière un proxy d'entreprise
L'installeur et Lyra ne lisent aucune variable qui leur soit propre pour le
réseau : ils respectent celles que les outils sous-jacents lisent déjà.
| Variable | Lue par | Effet |
|---|---|---|
| `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` | curl, pip/uv, requests, le script d'installation d'Ollama | tout le trafic sortant passe par le proxy |
| `PIP_INDEX_URL` (ou `UV_INDEX_URL`) | pip / uv | miroir PyPI interne pour le venv |
| `HF_ENDPOINT` | huggingface_hub (modèles Whisper, MiniLM) et le téléchargement des voix Piper | miroir Hugging Face interne |
| `OLLAMA_HOST` | le client `ollama` et Lyra (`get_ollama_base_url`, prime sur `llm.base_url` de config.yaml) | Ollama distant, machine sans GPU, Codespace. Précédence : `OLLAMA_HOST` > `llm.base_url` > `http://localhost:11434`. `--ollama-host` écrit les deux (`config.yaml` et un unique `export OLLAMA_HOST=` dans `~/.bashrc`, remplacé s'il existait déjà) ; un `OLLAMA_HOST` déjà exporté dans le shell courant l'emporte jusqu'à la prochaine session |
Trois endroits ne voyaient pas ces variables ; ils sont traités
(`installer/core/proxyenv.py`, `lyra/utils/netenv.py`) :
- **sudo** remet l'environnement à zéro : `sudo dnf install` / `sudo apt-get
install` partaient sans proxy. Toute commande `sudo` de l'installeur reçoit
`--preserve-env=`. Si ta politique sudoers l'interdit
(utilisateur sans droit `SETENV`), ajoute
`Defaults env_keep += "HTTP_PROXY HTTPS_PROXY NO_PROXY http_proxy https_proxy no_proxy"`
via `visudo`.
- **le service `ollama`** (systemd) n'hérite pas du shell, or c'est lui qui
télécharge lors d'un `ollama pull`. Avec un proxy défini, l'installeur écrit
`/etc/systemd/system/ollama.service.d/proxy.conf` (`Environment=HTTPS_PROXY...`),
recharge systemd et redémarre le service s'il tournait.
- **Lyra elle-même** : avec `HTTP_PROXY` seul, httpx et requests enverraient aussi
au proxy les appels à l'Ollama local, à l'API tracking (127.0.0.1:8765) et
aux serveurs MCP locaux. Le client et le démon ajoutent `localhost,127.0.0.1,::1`
à `NO_PROXY` au démarrage (seulement si un proxy est défini) ; les MCP lancés
par le démon en héritent.
Les mots de passe éventuels dans l'URL du proxy sont masqués dans les messages
de l'installeur.
Exemple :
```bash
export HTTPS_PROXY=http://proxy.interne:3128 NO_PROXY=localhost,127.0.0.1,.interne
export PIP_INDEX_URL=https://nexus.interne/repository/pypi/simple
export HF_ENDPOINT=https://hf-mirror.interne
export OLLAMA_HOST=gpu-box.interne:11434
python3 installer/install.py
```
## Machine sans Internet : bundle hors ligne
Limite actuelle : le bundle est une commande séparée, pas encore proposée par
l'installeur, et il n'a pas été rejoué de bout en bout sur une VM sans réseau
(suivi : issue [#25](https://github.com/amineutron/lyra/issues/25)).
`installer/bundle.py` met dans une seule archive (environ 3 Go) tout ce que
l'installeur télécharge : modèles Ollama, MiniLM (RAG) et Whisper (micro),
binaire et voix Piper, wheels Python du venv. Chaque fichier est contrôlé par
sha256 avant toute copie : une archive altérée ou tronquée est refusée en entier.
```bash
# machine connectée, depuis le dépôt (le venv fournit huggingface_hub)
.venv/bin/python -m installer.bundle create --out lyra-bundle.tar
# --parts ollama,hf,piper,wheels --whisper base|small|...|aucun
# machine sans réseau (python3 du système suffit)
python3 -m installer.bundle install lyra-bundle.tar --lyra-dir ~/lyra
LYRA_OFFLINE=1 # ensuite : interdit tout téléchargement de modèle
```
- Les modèles Ollama sont téléchargés par un serveur Ollama privé et temporaire
(`OLLAMA_MODELS` pointé vers le bundle) : ni sudo ni lecture du dossier du
service. À l'installation, la copie vers `/usr/share/ollama/.ollama/models`
passe par sudo si ce dossier n'est pas inscriptible.
- Les wheels sont celles de la plateforme de la machine qui crée le bundle
(même architecture et même version de Python attendues).
- Hors bundle : les paquets système (portaudio, git, nodejs...) et le binaire
Ollama, à installer depuis le média de la distribution ; les serveurs MCP.
- Au démarrage, Lyra charge les modèles Hugging Face depuis le cache local sans
interroger huggingface.co (`lyra/utils/hf_local.py`) ; le réseau ne sert que
si le modèle manque, et jamais avec `LYRA_OFFLINE=1` ou `HF_HUB_OFFLINE=1`.
## Rebuild du frontend app
```
make installer-ui # npm install + build -> app/backend/static/ (commite)
```
## Tests
```
.venv/bin/python -m pytest tests/installer/ -q
./installer/install.sh --tui --demo
./installer/install.sh --app --demo
```