ADR-033 — Modèle de configuration : source typée, compilation validée, artefact binaire
Statut : Accepté (2026-06-12). Décline les Principes d’ingénierie 4 (validation en amont, « parse, don’t validate »), 3 (zéro présomption sur les entrées) et 9 (budgets sur matériel modeste) en un modèle transverse de configuration système. Cohérent avec ADR-025 (builds reproductibles) et ADR-005 (systemd socle pour V1).
Catégorie : Architecture (transverse — couche 1 air-base-lib à couche 5
outils d’administration).
Contexte
Air s’adresse à deux publics qu’il refuse d’opposer : l’utilisateur qui ne veut pas savoir ce qu’il y a sous le capot et veut que « ça marche », et le développeur/technicien qui connaît sa machine et veut la régler finement. Les deux doivent être servis par le même système de configuration, sans que le premier puisse se tirer une balle dans le pied ni que le second soit bridé.
Le modèle Unix traditionnel — des fichiers texte éditables à la main sous /etc
— a une vertu (lisibilité, grep, diff, versionnable) et un vice symétrique :
une faute de frappe brique le système. Un grub.cfg erroné et la machine ne
boote plus ; un fichier de /etc corrompu et un pan entier de la sécurité tombe,
silencieusement. La validité n’est vérifiée qu’au runtime, trop tard, par le
consommateur, de façon non uniforme.
À l’inverse, un magasin binaire canonique sans source lisible (le Registre Windows) échange ces vices contre d’autres : opacité, fragilité à la corruption, impossibilité de diff/versionner, point de défaillance monolithique.
Air veut le meilleur des deux : la sûreté et la rapidité d’un format de données vérifié, et la lisibilité et la maîtrise du texte — sans le piège de l’un ni de l’autre.
Décision
La configuration Air a pour source de vérité une source typée validée à l’écriture par un compilateur de configuration, qui en dérive un artefact binaire reproductible que le runtime consomme. Le texte lisible (JSON/YAML/TOML) est une projection d’import/export, jamais la forme runtime ; le binaire est un artefact de build, jamais la source de vérité.
Sept règles gravées :
-
La source typée fait foi ; le binaire est dérivé. La source (forme texte typée, ou entrée structurée d’une GUI) est canonique, versionnable, diffable. Le binaire est un artefact reproductible qu’on peut détruire et reconstruire à l’identique (même esprit qu’ADR-025). Jamais l’inverse : inverser, c’est réinventer le Registre.
-
La compilation valide en amont (Principe 4). Le compilateur vérifie schéma, types et invariants avant de produire le binaire, refuse une entrée invalide, et émet des diagnostics de qualité compilateur (ligne/colonne, message actionnable, suggestion). Le runtime ne voit jamais de configuration invalide.
-
Le compilateur est tenu à la barre couches 0/1. Il ingère de la donnée humaine = donnée hostile (Principe 3) : fuzzing obligatoire, property-testing, couverture 100 %. La confiance se déplace du « sysadmin soigneux » vers cet outil, qui devient un composant sécurité-critique.
-
Artefact binaire schema-first à évolution intégrée. Le format doit gérer la compatibilité avant/arrière par construction (numérotation de champs, optionnalité — famille Cap’n Proto / FlatBuffers / protobuf, à auditer sous la règle des 80 %, ADR-024). Il porte version de schéma + checksum : une corruption est détectée, jamais lue en silence. Lecture zéro-copie / mmap privilégiée (budget runtime, Principe 9).
-
Projections texte round-trippables. Import/export JSON/YAML/TOML pour le développeur et le technicien ; outil de diff lisible entre deux générations. Ces formes alimentent ou exportent la source, mais ne sont pas lues par le runtime.
-
Sûreté système par générations + switch atomique + rollback. Une configuration valide peut exprimer une mauvaise intention (couper le réseau, mauvais paramètre kernel) : la validité ne suffit pas. Le filet anti-brick réel est le modèle générationnel — conserver la dernière configuration compilée known-good, basculer atomiquement (
air-filesystem::write_atomic), offrir un retour à la génération précédente (y compris une entrée de secours au boot pour le cas pré-userspace type bootloader). -
Backends de compilation = le joint d’indépendance. Le compilateur émet vers une cible enfichable. Pour V1 sur systemd (ADR-005), la cible est l’ensemble unit files + drop-ins
/etcgénérés et validés : l’humain édite la source Air, jamais l’unit file. Le jour où une brique systemd est remplacée par une brique Air native, on change le backend (émettre le binaire natif), sans toucher la source de l’utilisateur. Cf. la note de stratégie de remplacement progressif ci-dessous.
Conséquences
AirConfig(specair-base-libservices, aujourd’hui « parseur TOML derrière une feature ») est étendu vers ce modèle : un cœur validant unique,air-config, partagé par tous les frontends.- Deux publics, un cœur. Frontend texte typé piloté par le schéma (autocomplétion, doc inline, erreurs précises) pour le développeur ; GUI contrainte par le schéma — l’état invalide devient inexprimable — pour l’utilisateur lambda. Même validation, mêmes générations, même rollback.
- Couches touchées :
air-base-lib(couche 1, le cœurair-config),air-systemd(couche 2, backend unit files pour V1), les outils d’administration (couche 5), et les consommateurs commeair-device. - Articulation principes/ADRs : Principe 4 (valider en amont, parse don’t
validate), Principe 3 (entrée hostile → fuzzing), Principe 9 (lecture binaire
économe), ADR-025 (artefact reproductible), ADR-019 (les erreurs de compilation
sont des
AirErrorriches et contextualisées).
Alternatives rejetées
- Statu quo Unix (texte
/etccanonique, édité à la main). Lisible et versionnable, mais validité tardive et « typo qui brique » — le problème même qu’on résout. - Binaire canonique sans source texte (modèle Registre). Rapide mais opaque, fragile, non diffable, non versionnable.
- Format binaire maison sans politique d’évolution. Dette de schéma garantie sur l’horizon 20 ans ; d’où l’exigence schema-first (règle 4).
Hors périmètre (à traiter ailleurs)
L’authentification/intégrité des exécutables, bibliothèques et pilotes présents sur la machine (signature, contrôle de ce qui « doit » être là, exceptions développeur) est un second pilier distinct — adjacent au package management et au confinement OS — qui fera l’objet de son propre ADR. Ce document ne couvre que la configuration.
Amendement — les paramètres du noyau : l’équivalent Air de sysctl (2026-08-22)
Origine. Question du BDFL, posée en cherchant le domicile du manager d’air-config :
Ce qui n’a pas encore été abordé dans les ADRs mais qu’il va bien falloir traiter à un moment, c’est l’équivalent de
sysctl— et je pensais naturellement confier cette tâche àAirConfigManager.
Puis, sur la forme de la relation :
Il y a
air-system, le contrôleur qui sait comment parler au système, qui connaît les paramètres du système. PuisAirConfigManager, celui qui manage ce contrôleur… Maintenant la relation pourrait être inversée.
Ce que cet ADR disait déjà, sans en faire un objet. La règle 6 nomme le « mauvais paramètre kernel » comme l’exemple type d’une configuration valide qui exprime une mauvaise intention. Le sujet était donc vu — comme un risque, jamais comme un objet que le modèle sait porter. Cet amendement le fait entrer dans le périmètre.
Rectification préalable — deux affirmations que le code ne tient pas (2026-08-22)
Vérifié avant d’écrire, parce qu’un ADR qui se croit outillé et ne l’est pas est pire qu’un ADR muet :
- La règle 7 annonce un backend systemd de couche 2, et les « Conséquences » nomment une
crate
air-systemd. Elle n’existe pas. Les quatre backends de projection vivent dansair-config/src/backend/—accounts.rs,fstab.rs,resolv.rs,systemd.rs— donc en couche 1, à l’intérieur du cœur. L’intention de la règle 7 (le backend est le joint d’indépendance, on le change sans toucher la source de l’utilisateur) est tenue ; sa localisation annoncée ne l’est pas. - Les « Conséquences » situent le cœur dans
air-base-lib. Il vit dans une crate dédiée,air-config(couche 1), que dix crates consomment.
D-a — Un paramètre du noyau est de la configuration, et il entre dans ce modèle
La valeur voulue d’un paramètre du noyau est une donnée de configuration comme une autre : elle vit dans la source typée, la compilation la valide, et l’artefact binaire la porte. Rien dans les sept règles ne change pour elle.
Ce qui est nouveau, c’est que la valeur effective ne vit pas dans un fichier qu’Air écrit : elle vit dans le noyau en fonctionnement, et disparaît au redémarrage. Un paramètre du noyau a donc deux existences — celle qu’Air décide et celle que la machine porte à cet instant — et c’est cette dualité, absente partout ailleurs dans cet ADR, qui justifie les décisions qui suivent.
D-b — C’est la cinquième surface de projection — et le rollback y a une limite honnête
air-config projette déjà la configuration d’Air sur des surfaces étrangères qu’il ne
possède pas : /etc/passwd, /etc/fstab, /etc/resolv.conf, les drop-ins systemd. /proc/sys
est la cinquième, et le geste est le même. Générations, bascule atomique et retour à la
génération précédente (règle 6) s’y appliquent.
Mais le filet anti-brick n’y est pas complet, et l’écrire est le seul moyen de ne pas mentir.
Revenir en arrière sur un paramètre du noyau, c’est y réécrire l’ancienne valeur — ce qui suppose
qu’elle soit réécrivable. Certains paramètres ne le sont pas : le noyau documente
kernel.unprivileged_bpf_disabled comme à sens unique — mis à 1 ou 2, il ne se remet pas
sans redémarrer. (Propriété documentée du noyau ; elle n’a pas été vérifiée sur nos cibles, et
la vérifier demanderait de desserrer la sécurité d’une machine réelle.)
Conséquence gravée : un paramètre à sens unique se déclare comme tel dans le schéma, et son application n’est pas couverte par la promesse de rollback. Une génération qui en contient un franchit une frontière que le modèle générationnel ne sait pas défaire.
D-c — Air ne connaît que les paramètres que son schéma énumère
Jamais « écris cette valeur à ce chemin sous /proc/sys ». Chaque paramètre est un champ
nommé et typé du schéma, portant son chemin, son type, le sens dans lequel il durcit, et son
caractère à sens unique éventuel.
Deux raisons, et chacune suffirait :
-
Principe 11. Un écrivain de chemin générique est exactement le parseur tolérant sur le chemin d’entrée d’une décision que le principe proscrit : il accepte ce qu’il ne comprend pas, et deux lecteurs en tirent deux conclusions.
-
Sans le schéma, la garde de D-e est impossible — le sens du durcissement n’est pas dérivable de la valeur. Mesuré sur carbon le 2026-08-22 :
Paramètre Lu Sens qui durcit kernel.dmesg_restrict1 monter kernel.kptr_restrict1 monter kernel.yama.ptrace_scope1 monter kernel.randomize_va_space2 monter fs.protected_symlinks1 monter vm.mmap_min_addr65536 monter kernel.perf_event_paranoid4 monter — et l’échelle descend à −1 : le champ est signé kernel.unprivileged_bpf_disabled2 monter — et à sens unique (cf. D-b) net.ipv4.ip_forward1 descendre ip_forwardest le contre-exemple qui ferme la porte : « plus grand = plus sûr » est faux. Aucun moteur générique ne peut juger ; seul le schéma le sait. -
Un paramètre n’est même pas toujours une échelle. Relevé le 2026-08-22 sur les deux arches tier-1 (carbon x86_64, raspi-srv-2 aarch64) : les dix-neuf paramètres candidats existent sur les deux, mais ils ne se comparent pas de la même façon. Le schéma distingue donc trois espèces, et la garde de D-e s’énonce dans chacune :
Espèce Ce que « desserrer » veut dire Exemple Échelle franchir la valeur dans le sens qui affaiblit — sens porté par le champ kernel.dmesg_restrict(monter durcit),net.ipv4.ip_forward(descendre durcit)Seuil descendre sous le plancher en vigueur vm.mmap_min_addr— 65536 sur x86_64, 32768 sur aarch64 : le plancher sûr dépend de l’architecture et ne se grave pasMasque de bits allumer un bit éteint kernel.sysrq= 176 sur les deux exécuteurs, soit0b10110000— ni une échelle ni un seuilUn masque de bits enterre définitivement l’idée d’une comparaison numérique unique : desserrer
sysrqde 176 à 180 ajoute une fonction, alors que 180 > 176 comme 176 > 172. La comparaison est par espèce, jamais générique.Deux autres valeurs diffèrent entre les exécuteurs sans que l’une soit plus sûre dans l’absolu —
fs.suid_dumpable(0 / 2) etnet.ipv4.ip_forward(1 / 0) — ce qui confirme qu’Air décrit un sens, jamais une valeur universelle attendue.
D-d — Le savoir est un contrôleur interne d’air-system ; la décision est au AirConfigManager
La question posée était « qui manage qui ». Le test d’ADR-149 D4 n’est pas la hiérarchie, c’est le contrat : un manager est la surface publique que les toits appellent ; un contrôleur interne est le secret d’implémentation, « destiné à évoluer, changer, disparaître ».
Les deux contrats sont déjà tenus, et l’inversion en casserait un : air-libc-system
(src/lib.rs:35) bind déjà AirSystemManager pour gethostname/sethostname ; air-config
est consommé depuis la couche 2 par air-sshd, air-launchd, air-bundle et six autres.
air-system et air-config restent tous deux des managers.
Ce qui est authentiquement un contrôleur, c’est la table des paramètres — chemin, type, sens
du durcissement, sens unique. C’est du savoir système pur, qui évolue avec les versions du
noyau : la définition littérale du contrôleur interne. ADR-149 D6 l’autorise sans réserve
(« une crate peut regrouper 1, 2, 3 ou N contrôleurs ») : air-system ne devient pas un
contrôleur, il en gagne un, et garde son manager.
Elle vit dans air-system, pas dans air-config. air-sys-syscall lit déjà
/proc/sys/kernel/apparmor_restrict_unprivileged_userns (l. 4052) et
/proc/sys/kernel/yama/ptrace_scope (l. 4859) dans ses harnais : les futurs consommateurs de
cette table ne sont pas le domaine de la configuration. Si elle y vivait, air-sandbox
devrait dépendre de la configuration pour demander « ce noyau autorise-t-il les userns non
privilégiés ? ». ADR-149 D7 autorise explicitement la consommation intra-couche entre
contrôleurs, qui est le chemin retenu.
Et c’est le caractère interne qui rend la garde incontournable. ADR-149 D9 interdit à la
couche 2 de voir un contrôleur : un service qui veut toucher un paramètre ne peut passer que
par AirConfigManager, donc par la garde de D-e. Si la table était publique, n’importe quel
service desserrerait kernel.dmesg_restrict sans qu’aucune politique soit consultée. La porte
n’est pas incontournable parce que les appelants sont vertueux : elle l’est par construction.
Le partage est donc :
| Ce qu’il détient | |
|---|---|
Contrôleur interne d’air-system | Où vit le paramètre, de quel type, dans quel sens il durcit, s’il est à sens unique. Mécanisme. Libre d’évoluer avec le noyau. |
AirSystemManager | Inchangé : l’identité système à syscall dédié (hostname, nombre de processeurs). |
AirConfigManager | Quelle valeur, d’où elle vient, quand elle s’applique, et ce qui se passe si elle desserre. Politique — donc il porte la garde. |
D-e — La garde porte sur le desserrage, jamais sur le durcissement
C’est la monotonie, encore — la même forme qu’à ADR-112 et ADR-150 D3 : durcir un paramètre ne peut pas affaiblir la machine ; le desserrer, si. Beaucoup de ces paramètres sont les défenses du noyau.
L’opération soumise au SecurityManager (ADR-089) est donc le desserrage, et elle porte le
paramètre, sa valeur actuelle et la valeur demandée — jamais un simple « on écrit ». Ce que
« desserrer » signifie se lit dans l’espèce du paramètre (D-c) : franchir dans le mauvais sens
pour une échelle, descendre sous le plancher pour un seuil, allumer un bit pour un masque. Une politique
peut alors dire ce qu’aucune n’a pu dire jusqu’ici :
Sur cette machine, aucun paramètre du noyau ne se desserre — quel que soit le motif invoqué.
Le durcissement n’est pas gardé, et c’est délibéré : une politique qui refuserait de durcir ferait de la garde le vecteur qu’elle prétend fermer.
D-e-bis — La projection sur /proc/sys ne peut pas être atomique
Les quatre backends existants écrivent par air_filesystem::AirFileSystem::write_atomic —
fichier temporaire puis rename, ce qui rend la bascule indivisible (règle 6). procfs ne
supporte pas rename : la cinquième surface ne peut pas emprunter ce mécanisme. Une génération
qui touche N paramètres les écrit donc un par un, et une interruption au milieu laisse la
machine dans un état intermédiaire qu’aucun rename ne rattrape.
C’est la seconde limite de D-b, et elle a la même cause : cette surface n’est pas un système de fichiers, c’est une interface du noyau qui y ressemble. L’écrire ici évite qu’un lecteur suppose l’atomicité par analogie avec les quatre autres backends.
D-f — Volatile et durable sont deux opérations, jamais un drapeau
sysctl a deux visages : l’application immédiate et non persistante (-w), et l’inscription qui
survit au redémarrage (/etc/sysctl.d). Air garde les deux, séparés :
- l’inscription modifie la source typée, se recompile, et prend effet par une génération ;
- l’application volatile touche le noyau maintenant, ne survit pas au redémarrage, et se journalise comme telle.
Elles ne sont jamais deux modes d’un même appel distingués par un drapeau. Une machine où l’on ne distingue pas « appliqué » de « inscrit » est une machine qui ment à son administrateur — et elle ne lui ment qu’au redémarrage suivant, c’est-à-dire au pire moment.
Hors périmètre de cet amendement
Qui applique les paramètres au démarrage — PID 1, le lanceur, ou une unité dédiée — n’est pas tranché ici. C’est une question d’amorçage, adjacente à ADR-005 et au lanceur, et l’inventer au passage serait exactement le raccourci que ce dépôt refuse.
Licence du document : MPL 2.0 Statut : Accepté. Édition directe autorisée en phase de design pré-ouverture publique ; après ouverture publique, toute évolution passe par RFC (ADR-015).