Spec couche 1 — air-config : schémas .capnp v1 + BNF du format de source (design)
Spécification technique — Version 0.2 (décisions validées BDFL 2026-06-25 ; v1 = réseau/
services/comptes + mounts/fstab). Couche 1. Dernier pré-requis avant implémentation
d’air-config (cf. air-config.md §« pré-implémentation » et
air-config-source-format.md).
Objet
Définit (A) les schémas Cap’n Proto des domaines de config v1 (alignés sur les
backends validés : resolv.conf+hosts, systemd, passwd/shadow — décision #2) et
(B) la BNF gelée du format de source. Ensemble = la matière prête pour
l’implémentation.
Conventions de schéma (communes à tous les domaines)
- Annotations Air (fichier
air-config-annotations.capnp) :@0xada1ada1ada1ada1; annotation secret @0xacf0...01 (field) :Void; # champ = secret → zeroize, jamais loggé annotation domain @0xacf0...02 (struct) :Text; # nom de domaine ("air.network", "air.accounts"…) annotation backend @0xacf0...03 (struct) :Text; # backend /etc cible ("resolv.conf"…) - Enveloppe versionnée (en-tête commun, ADR-040 §version+checksum) :
struct ConfigEnvelope { schemaDomain @0 :Text; # ex. "air.network" schemaVersion @1 :UInt32; # version du schéma de domaine (évolution) # checksum : calculé sur l'artefact, porté HORS payload (en-tête de fichier), pas ici payload @2 :AnyPointer; # le struct racine du domaine } - Optionnalité : Cap’n Proto n’a pas d’
Optionnatif → convention « champ absent = valeur par défaut du schéma » ; pour distinguer non-posé de valeur par défaut quand c’est sémantiquement requis, on utilise un groupe unionunset | set :T. - Types réutilisés :
struct IpAddress { union { v4 @0 :UInt32; v6 @1 :Data; } } # v6 = 16 octets struct Duration { millis @0 :UInt64; } # source : sucre 5s/250ms → millis
(A) Schémas v1
A.1 — Domaine air.network (backends resolv.conf + hosts)
@0x0001...; # file id (à générer)
using Air = import "/air-config-annotations.capnp";
struct NetworkConfig $Air.domain("air.network") {
resolver @0 :ResolverConfig;
hosts @1 :HostsConfig;
}
struct ResolverConfig $Air.backend("resolv.conf") {
nameservers @0 :List(IpAddress); # ordonné (priorité)
searchDomains @1 :List(Text);
ndots @2 :UInt8; # option resolv.conf
timeout @3 :Duration;
attempts @4 :UInt8;
ipv6First @5 :Bool;
}
struct HostsConfig $Air.backend("hosts") {
entries @0 :List(HostEntry);
}
struct HostEntry {
address @0 :IpAddress;
names @1 :List(Text); # canonique d'abord, puis alias
}
A.2 — Domaine air.services (backend systemd)
@0x0002...;
struct ServicesConfig $Air.domain("air.services") $Air.backend("systemd") {
units @0 :List(UnitConfig);
defaultTarget @1 :Text; # → /etc/systemd/system/default.target (symlink généré)
}
struct UnitConfig {
name @0 :Text; # ex. "sshd.service"
state @1 :UnitState; # enabled → *.wants/ ; masked → symlink /dev/null
dropIns @2 :List(DropIn); # → /etc/systemd/system/<unit>.d/*.conf
}
enum UnitState { disabled @0; enabled @1; masked @2; }
struct DropIn { name @0 :Text; section @1 :Text; key @2 :Text; value @3 :Text; }
systemd : air-config ÉMET ces fichiers (
*.wants, drop-ins) ; l’humain ne fait plussystemctl enable(ADR-041). systemd ne fait que lire.
A.3 — Domaine air.accounts (backends passwd + shadow) — LE PLUS SENSIBLE
@0x0003...;
struct AccountsConfig $Air.domain("air.accounts") {
users @0 :List(User);
groups @1 :List(Group);
}
struct User $Air.backend("passwd") {
name @0 :Text;
uid @1 :UInt32;
gid @2 :UInt32;
gecos @3 :Text;
home @4 :Text;
shell @5 :Text;
# --- champs shadow ---
passwordHash @6 :Text $Air.secret; # SECRET : zeroize, jamais loggé, backend `shadow` (0600)
lastChange @7 :UInt32; # jours depuis epoch
minAge @8 :UInt32;
maxAge @9 :UInt32;
# … warn/inactive/expire selon besoin
}
struct Group $Air.backend("passwd") {
name @0 :Text;
gid @1 :UInt32;
members @2 :List(Text);
}
$Air.secretsurpasswordHash⇒ le compilateur zeroize ce champ dans ses tampons après usage, l’élide des diagnostics, et le backend l’écrit dans/etc/shadow(mode 0600), jamais danspasswd. Domaine livré en dernier (décision #2).
A.4 — Domaine air.mounts (backend fstab)
@0x0004...;
struct MountsConfig $Air.domain("air.mounts") $Air.backend("fstab") {
entries @0 :List(MountEntry);
}
struct MountEntry {
source @0 :Text; # device / UUID= / LABEL= / partage réseau
target @1 :Text; # point de montage
fsType @2 :Text; # ext4, btrfs, nfs, tmpfs…
options @3 :List(Text); # rw, noatime, nofail…
dumpFreq @4 :UInt8; # champ 5 de fstab
passNumber @5 :UInt8; # champ 6 (ordre fsck)
}
fstab= table de montage STATIQUE (config)./etc/mtab(mnttab) est EXCLU : c’est de l’état runtime (symlink kernel vers/proc/self/mounts), pas de la config → air-config n’y touche pas. ⚠️ Unfstaberroné peut bloquer le boot : les générations + rollback + entrée de secours au boot (ADR-033 §6) sont ici critiques.
Backends candidats (périmètre PROGRESSIF, ADR-041 — hors v1 strict)
À ajouter ensuite, par ordre de pertinence : nsswitch.conf (lie résolution réseau ↔
comptes), locale.conf/hostname/localtime(tz)/vconsole.conf (domaine air.locale/
air.hostname), sysctl.d, crypttab (pair de fstab). À DIFFÉRER (très sensibles) :
sudoers, pam.d (escalade/auth — prudence façon passwd). Hors air-config-cœur :
sshd_config (→ config du service air-ssh, couche réseau).
(B) BNF gelée du format de source
Formalise air-config-source-format.md (schema-directed).
document = header , { line } ;
header = "schema" , ws , "=" , ws , string , newline ; (* obligatoire, en tête *)
line = ( comment | section | array_table | assignment | blank ) , newline ;
comment = "#" , { any_char_no_nl } ;
blank = { ws } ;
section = "[" , path , "]" ;
array_table = "[[" , path , "]]" ;
path = ident , { "." , ident } ;
assignment = ws , key , ws , "=" , ws , value ;
key = ident ;
ident = alpha , { alpha | digit | "_" | "-" } ;
value = bool | integer | float | string | bytes | enum_val | list ;
bool = "true" | "false" ;
integer = [ "-" ] , ( dec_int | hex_int ) ; (* checked vs type schéma *)
dec_int = digit , { digit | "_" } ;
hex_int = "0x" , hexdigit , { hexdigit | "_" } ;
float = [ "-" ] , digit , { digit } , [ "." , digit , { digit } ] , [ exp ] ;
exp = ("e"|"E") , [ "+" | "-" ] , digit , { digit } ;
string = '"' , { str_char | escape } , '"' ;
escape = "\" , ( "n" | "t" | "r" | '"' | "\" | "u{" , hexdigit{1,6} , "}" ) ;
bytes = ( "hex:" , string ) | ( "b64:" , string ) ;
enum_val = ident ; (* validé vs enum du schéma *)
list = "[" , ws , [ value , { ws , "," , ws , value } , [ "," ] ] , ws , "]" ;
duration = dec_int , ( "ms" | "s" | "m" | "h" | "d" ) ; (* sucre, si champ Duration *)
size = dec_int , ( "B" | "KiB" | "MiB" | "GiB" ) ; (* sucre, si champ taille *)
(Bornes anti-DoS : taille document/ligne, profondeur de path, longueur de list —
appliquées par le lexer, cf. air-config-source-format §1. Clés dupliquées = erreur.)
Mapping lexème → type Cap’n Proto (schema-directed)
| Champ schéma | Lexèmes acceptés |
|---|---|
Bool | true/false |
UInt8..64/Int8..64 | integer (checked_* vs largeur+signe) |
Float32/64 | float (+ integer promu) |
Text | string |
Data | bytes (hex:/b64:) |
enum | enum_val (∈ valeurs schéma ; sinon did-you-mean) |
List(T) | list (homogène T) ; ou [[table]] pour List(Struct) |
struct | [section] |
Duration/taille | duration/size (sucre) ou integer |
$secret | string — zeroizé, élidé des diagnostics |
Décisions validées (BDFL 2026-06-25)
- Extension de fichier =
.airconf. ✓ - Sucre
duration/size(5s,10MiB) inclus v1. ✓ — unités explicites ; la conversion vers l’entier cible estchecked_*→ tout overflow rejeté (diagnostic). - Texte multi-lignes
"""…"""inclus (drop-ins, gecos). ✓ - IP = struct typé
IpAddress(union v4/v6). ✓ - File ids
.capnpgénérés (capnp id) au câblage. ✓ fstabajouté (domaineair.mounts, A.4)./etc/mtabexclu (état runtime). ✓ Autres backends → périmètre progressif (cf. « Backends candidats »).
Travail à reprendre (→ implémentation)
- Générer les file ids + figer les
.capnpdansair-config-schema/; committer le.rsgénéré (ADR-040). - Implémenter
air-config-compile(lexer/parseur BNF ci-dessus + validateur schema-directed) — fuzzé ; puis le lecteur runtime + générations + backends (resolv.conf/hosts d’abord).
Licence du document : MPL 2.0
Statut : v0.2 — schémas .capnp v1 (réseau/services/comptes/mounts) + BNF gelée du
format de source. Décisions validées (extension .airconf, sucre durée/taille checked,
multi-lignes, IP struct, fstab ; mtab exclu). Prêt pour implémentation : câbler
air-config-schema (file ids + code généré committé) puis air-config-compile (fuzzé).
Bases : ADR-033 / ADR-040 / ADR-041 + specs air-config + air-config-source-format.