ADR-077 — Managers de domaine en couche 1 (FileManager/NetworkManager/TerminalManager…) : surface objet médiatrice des toits (libc + PAL)
Statut : Accepté (2026-07-08, décision BDFL directe). Complète ADR-052 (objets couche 1), renforce la stratification (Principe 1). Encadre un descellement additif de la couche 1 (RFC, ADR-015/051/065).
Catégorie : Architecture des toits sur couche 1. Ajoute de l’API couche 1 (additif) ; n’altère aucune API existante.
Généralisé par ADR-149 (« Architecture de code : contrôleurs, managers de domaine, et contrat d’API », 2026-08-03). Le présent ADR traite des managers de couche 1 ; ADR-149 pose la doctrine dont ils sont un cas particulier — le contrôleur comme unité de responsabilité et de test, les trois rangs (manager public / contrôleur spécialisé / contrôleur interne jamais public), le contrat d’API réduit aux managers et types publics, et la règle « une crate porte au moins un contrôleur ». L’état de conformité relevé dans l’arbre reste ici, dans l’amendement de fin de document.
Contexte
Règle non négociable : les toits (libc C-ABI, PAL std::sys) s’appuient EXCLUSIVEMENT sur la couche 1, jamais sur la couche 0 (air-sys-syscall). Seule la couche 1 parle à la couche 0.
Un audit (campagne libc-pour-std, chantier B) révèle deux crates libc qui violent la règle : air-libc-fileio (8 appels air_sys_syscall::{fs,process,ipc,system}) et air-libc-socketio (9 appels air_sys_syscall::net — codec sockaddr + sockopt, descellés en couche 0 pour la libc par ADR-070/071 : le pattern même à proscrire). Les 4 autres crates libc sont déjà propres.
Par ailleurs, la couche 1 est incomplète pour la surface visée : air-filesystem a une API riche AirFile (fd) mais pas les opérations par chemin (mkdir/unlink/rename/symlink/readlink/chmod/chown/access), ni chdir/getcwd ; air-socket/air-handle n’exposent pas le codec sockaddr ni les sockopt.
Décision
1. Ce qui manque en couche 1 s’implémente via une VRAIE API OBJET — des Managers de domaine. La couche 1 expose, par domaine, un objet Manager médiateur qui est LA surface consommée par les toits :
AirFileManager(air-filesystem) — tout l’I/O fichier : ouverture, ops par chemin (mkdir/unlink/rename/symlink/readlink/chmod/chown/access/hardlink/mkfifo/truncate/sync), répertoires, métadonnées par chemin,chdir/getcwd, fabrique de temporaires.AirNetworkManager(air-socket/air-handle) — tout le réseau : sockets, codecsockaddr↔ octets,sockopt, résolution de noms, transfert.AirTerminalManager(air-terminal) — termios/tty/PTY (cf. ADR-060/061).- … (par domaine : Process, Time…), au fil des besoins.
Le Manager prend/rend des types couche 1 (AirPath, Mode, AirResult, newtypes FD), jamais des types C. Il coordonne les briques existantes (AirFile, registre air-handle, AirSocket…) et la couche 0. La libc et le PAL bindent le Manager (le shim libc n’y ajoute que l’adaptation C-ABI : CStr↔AirPath, errno↔AirResult). Une brique couche 1 → 2 faces ([ADR-051] doctrine dual-face).
2. Correction en UN SEUL cycle de descellement. (a) Desceller la couche 1 une fois ; (b) implémenter les Managers manquants (AirFileManager, AirNetworkManager) et recâbler air-libc-fileio + air-libc-socketio dessus, en retirant leur dépendance air-sys-syscall ; (c) tests (barrière verte, couverture couche 1 à 100 %) ; (d) si OK, re-sceller la couche 1 une fois (couche-1-v1.x). Pas de descellements multiples en série.
Conséquences
- ADR-070/071 révisés en pratique : le codec
sockaddret lessockoptrestent en couche 0 (ils y sont légitimes comme wrappers typés), mais la libc ne les appelle plus directement — elle passe parAirNetworkManager(couche 1), qui, lui, peut consommer la couche 0. La frontière proscrite (libc→couche 0) disparaît. - Fondation des 71 symboles
std: la campagne libc-pour-stdbind désormais les Managers ; les ops manquantes s’ajoutent au Manager (couche 1), puis au shim — jamais un raccourci couche 0. - Coût : descellement + re-sceau couche 1 (additif, gouverné) ; extension d’
air-filesystem/air-socket(tests 100 %) ; recâblage de 2 crates libc (ABI C inchangée). - Réversibilité : purement additif côté couche 1 ; les toits gagnent une surface plus propre. Aucune API scellée retirée.
Alternatives rejetées
- Free-functions couche 1 éparses (au lieu de Managers objet) : rejeté — la directive BDFL exige une API objet cohérente par domaine (découvrabilité, invariants portés par l’objet, parité libc/PAL).
- Grand-fathering de
air-libc-fileio/socketio: rejeté (choix BDFL « remédier maintenant »). - Laisser la libc taper la couche 0 (statu quo ADR-070/071) : rejeté — casse la stratification, l’instrumentation et la doctrine dual-face.
Amendement — état de conformité des Managers, relevé dans l’arbre (2026-08-03)
Demandé par le BDFL le 2026-08-03 : « reprendre chaque ADR nommant ces Managers et corriger pour mettre en conformité leur état, leur rôle, leur fonctionnement ». Cette section devient l’état faisant autorité ; les autres ADR y renvoient plutôt que de le dupliquer.
Le critère retenu — corrigé le 2026-08-03 (second passage)
Première formulation, trop faible. Le premier relevé demandait qu’« aucune opération du domaine ne soit atteinte par un toit autrement que par le Manager », et comptait les sites d’appel dans
crates/air-libc-*. Ce critère mesure au mauvais endroit. Un toit qui, ce jour-là, n’emprunte pas une route ne prouve rien : la route existe, et le prochain binding — C++, Swift, un langage qu’on n’a pas prévu — la trouvera. Rappel du BDFL, 2026-08-03 : les Managers sont des contrôleurs de très haut niveau destinés à recevoir tous les appels des bindings, et à cacher l’implémentation interne. Une route non empruntée reste une fuite d’implémentation.
Le critère porte donc sur ce que la couche 1 expose, non sur ce que les toits appellent :
Un domaine est conforme si aucune opération du domaine n’est publique en dehors de son Manager. Le Manager peut exposer des types (
AirSocket,PtyPair,AirDevice…) — c’est même sa monnaie. Ce qu’il ne doit pas laisser fuir, ce sont les opérations : ni moduleenginepublic, nicreate/bind/listenappelables directement, maisAirNetworkManager::create_socket(),::bind_socket(),::listen_socket().
Corollaire, et il autorise ce qui existe. Une fonction en forme de C — openpty(),
tcgetattr() — a le droit d’exister, parce que la libc doit fournir ses fonctions standard.
Mais elle n’est légitime que comme coquille de traduction : son corps doit appeler
AirTerminalManager::open_pty(), et rien d’autre. Ce qu’ADR-077 §1 disait déjà — « le shim libc
n’y ajoute que l’adaptation C-ABI » — et qu’il faut lire comme une interdiction du chemin
parallèle, pas comme une simple préférence de style.
Le relevé est reproductible : pour chaque crate de couche 1, on liste les pub fn libres qui
sont des opérations de domaine (par opposition aux helpers purs), et on les compare à la
surface du Manager.
Ce que le relevé donne
Le modèle, pour situer les autres. AirFileManager porte 45 méthodes, et air-filesystem
n’expose que trois fonctions libres — coalesce, map_event_kind, segment_matches — qui
sont des helpers purs, pas des opérations de domaine. C’est la doctrine appliquée telle
qu’elle est écrite.
| Domaine | Contrôleur | Opérations du domaine exposées hors du contrôleur | Verdict |
|---|---|---|---|
air-filesystem | AirFileManager (45) | 0 — trois helpers purs seulement | Conforme. La référence |
air-env | AirEnvironmentManager (4) | 0 fonction libre publique dans la crate ; des doublons subsistent côté toits | Quasi conforme |
air-system | AirSystemManager (3) | 0 | Conforme (domaine étroit) |
air-memory | AirMemoryManager (3) | raw_mapping | Un trou |
air-log | AirLogManager (10) | 1 | Conforme ; clients en couche 2, pas des toits libc |
air-signal | AirSignalManager (2) | 12 — block, unblock, replace_mask, current_mask, pending, raise_signal, queue_signal, send_signal_to_process, send_signal_to_process_group, send_signal_to_thread, wait_for_signal, wait_for_signal_until | Non conforme |
air-process | AirTaskManager (16) | 14 — dont toute la séparation de privilèges : drop_privileges, drop_to_user, set_uids, set_gids, set_supplementary_groups, supplementary_groups, reduce_capabilities_to, close_inherited_fds_except ; plus spawn_process, exit_process, signal_by_pidfd, parent_death_signal, set_parent_death_signal, air_pipe | Non conforme — et c’est le plus grave |
air-socket | AirNetworkManager (4, utilitaires) | le module engine est public : create, bind, listen, connect, accept, send, receive, send_message, receive_message, shutdown, local_address, peer_address, peer_credentials, socket_pair, set_reuse_address, set_no_delay, set_keep_alive, set_nonblocking, is_connect_in_progress — plus AirSocket et ses 65 méthodes | Non conforme |
air-terminal | aucun | 21 — openpty, open_pty_master, pty_peer, pty_number, pty_unlock, tcgetattr, tcsetattr, tcdrain, tcflow, tcflush, tcsendbreak, tcgetpgrp, tcsetpgrp, make_raw, isatty, ttyname, session_id, set_controlling_terminal, clear_controlling_terminal, window_size, set_window_size | Non conforme, cas extrême |
air-process est le plus préoccupant, et pas par le nombre. C’est la séparation de
privilèges — Principe 10 — qui est intégralement hors contrôleur. drop_privileges et
set_uids sont exactement ce qu’un contrôleur doit médier plutôt qu’offrir en libre-service : un
binding qui les appelle directement contourne tout ce qu’un SecurityManager
(ADR-089) pourrait un jour vouloir consulter.
AirTerminalManager::open_pty() n’existe pas — le contrôleur lui-même n’existe pas. Le chemin
réel est air_terminal::openpty() → air_terminal::open_pty_master() →
air_sys_syscall::terminal::open_pty_master() : trois étages, aucun contrôleur.
Point qui reste vrai : aucune violation de stratification. Aucun toit ne dépend
d’air-sys-syscall, et une crate de couche 1 doit consommer la couche 0 — c’est son rôle.
L’infraction n’est pas là : elle est au franchissement, dans ce que la couche 1 rend public.
Les Managers absents
| Manager | Crate | Ce que le domaine expose | Gravité |
|---|---|---|---|
AirTerminalManager | air-terminal | AirTerminal, PtyPair, AnsiCodec, mais l’essentiel en fonctions libres (openpty, pty_*, tcgetpgrp…) | Forte — nommé explicitement au §1 du présent ADR. Rendez-vous au jalon « un shell sur console série » |
AirSandboxManager | air-sandbox | AirSandbox (poseur de profil, apply()), AirSandboxProfile, AirLandlockPolicy, deux fonctions de compilation | Moyenne — ADR-112 le nomme dans son propre titre alors que le code livre AirSandbox. Bloqué par l’ADR sécurité dédié (cf. amendement D6 d’ADR-046) |
AirDeviceManager | air-device | AirDevice, AirDeviceEnumerator, AirDeviceMonitor, AirBlockDevice — surface objet complète, aucune fonction libre | Faible — il manque le point d’entrée médiateur, pas les objets |
AirKeystoreManager | air-keystore | Des types par artefact (AirHostKey, AirKnownHost, AirCaKey, AirKrl, AirRevocationStore, AirSshKeyPair…) et 25 fonctions libres par module (load, save, encode_artifact, decode_artifact, verify_host, is_trusted_ca, is_revoked) | Moyenne, et d’une autre nature. ADR-108 §1 déclare ce domaine « manager de domaine au sens d’ADR-077 » et le manifeste affirmait une surface médiatrice « jamais des free-functions » — les deux étaient faux. Mais aucune de ces 25 fonctions n’atteint la couche 0 : elles passent toutes par AirFileManager. Ce n’est donc pas une route parallèle vers le noyau, c’est une façade manquante pour les bindings |
Quatre Managers que la doctrine ne nommait pas
Le §1 énumérait AirFileManager, AirNetworkManager, AirTerminalManager. Le code en a produit
quatre autres, légitimes mais jamais doctrinés : AirEnvironmentManager, AirSignalManager,
AirMemoryManager, AirSystemManager. Ils sont par la présente reconnus comme
Managers de domaine au sens de cet ADR, avec les mêmes obligations — dont celle de médier.
Noms périmés
AirProcessManager→ renomméAirTaskManagerpar ADR-088 («stdsur PAL safe en couche 1, sans C »), pour coller au vocabulaire du noyau : task + thread group. Le renommage est allé au bout dans le code ; des mentions résiduelles subsistaient dans les ADR et les manifestes au 2026-08-03.AirThreadManager→ jamais créé, dissous dansAirTaskManagerpar la même décision (forket création de thread sont le mêmeclone3, à des drapeaux près).
Ce que cet amendement ordonne, et ce qu’il laisse ouvert
Tranché par le BDFL le 2026-08-03 — les Managers absorbent leur domaine. La question posée par la première version de cet amendement (« absorber, ou rétrograder le Manager au rang d’utilitaire assumé ? ») est close dans le premier sens. Un Manager est un contrôleur de très haut niveau : il reçoit tous les appels des bindings — C, C++, Swift, et ce qui viendra — et il cache l’implémentation interne. Le §1 n’est donc pas à amender à la baisse ; c’est le code qui doit s’y conformer.
Ce qui en découle mécaniquement, et qui reste à ordonnancer :
air-terminal: écrireAirTerminalManager, y porter les 21 opérations, et ne garder les fonctions en forme de C que comme coquilles de traduction. Rendez-vous au jalon « un shell sur console série » ;air-socket: dépublier le moduleengine, porter ses 19 opérations surAirNetworkManager(create_socket,bind_socket,listen_socket…),AirSocketrestant un type légitimement exposé ;air-process: porter la séparation de privilèges surAirTaskManager— c’est la plus urgente, parce qu’une opération de privilège en libre-service contourne par construction tout arbitrage de sécurité ultérieur ;air-signal: porter les 12 opérations surAirSignalManager;air-keystore,air-device,air-sandbox: écrire le contrôleur manquant.
Trois réserves subsistent, et elles ne sont pas de la même eau :
AirSandboxManagerest bloqué, non déprioritisé — sa surface dépend d’arbitrages renvoyés par l’amendement D6 d’ADR-046 à un ADR sécurité dédié.- Le coût de re-scellement. La couche 1 est scellée ; déplacer des opérations d’une fonction libre vers un Manager change la surface publique. À mener en un seul cycle de descellement, comme le §2 l’a fait la première fois — pas en série.
- Les objets déjà bien formés (
AirSocket,AirDevice,AirTerminal) ne sont pas en cause : ce sont des types, et un contrôleur a vocation à en rendre. Seules les opérations doivent cesser d’être joignables autrement que par lui.