Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Air — Journal de bord

Archive chronologique append-only (détail session par session, PR par PR). Pour l’état vivant « où en est-on / prochaine tâche / tâches à venir », le document de suivi est etat-avancement.md — à lire en premier. Ce journal reste la trace historique fine.

2026-07-10 — 🎉 M5 ATTEINT : std tourne sur Air, zéro glibc, 2 arches

Jalon majeur : un programme Rust std (hello-std) lié intégralement contre libair (zéro glibc) s’exécute on-targethello from std on air + exit 0 sur x86_64-air (carbon natif) ET aarch64-air (raspi natif). Static-PIE, pari ADR-076 tenu de bout en bout.

Le lien de hello-std révélait 12 symboles indéfinis ; comblés en 6 PR :

  • #308 __xpg_strerror_r (alias XSI de strerror_r).
  • #309 open/fcntl — exports non-variadiques cible (#[cfg(target_vendor="air")]).
  • #310 couche 1 air-runtime::process_context (capture auxv + bornes de pile au bootstrap) + AirProcessManager::resource_limitADR-086.
  • #311 getauxval + pthread_self + détection de pile (pthread_getattr_np/ attr_getstack/attr_getguardsize).
  • #312 TLS keys cible (pthread_key_create/delete/setspecific) via #[thread_local] natif (discipline .tbss) + allocateur KEYS extrait/partagé.
  • #313 syscall générique — couche 0 raw_syscall (asm 2 arches) + shim libc cible. EXCEPTION assumée temporaire (ADR-087) : entorse à ADR-021 conv.3 + « libc binde couche 1 » ; à retirer avec l’équipe Rust (le pal unix de std appelle futex/gettid en direct).

Re-sceaux (PR #314, tags signés GPG) : couche-0-v1.12 (raw_syscall) + couche-1-v1.9 (introspection thread ; l’earmark ADR-078 crypto glisse en v1.10). Prochaine phase = OpenSSH / air-sshd (ADR-074). (La campagne M4 réseau + posix_spawn, 2026-07-05→09, n’a pas d’entrée dédiée ici — voir etat-avancement.md et le registre des ADR.)

2026-07-04 — 🧵 libc M3 Phase B : familles signaux + pthread (couche 2)

Toit libc M3 Phase B (branche feat/libc-m3-phaseB, sur la Phase A ADR-066). Deux crates de logique mesurée (aucun #[no_mangle]), empilement strict libc → couche 1 (jamais air-sys-syscall) : air-libc-signal (sur air-signal) et air-libc-thread (sur air-thread::raw_futex). Shims extern "C" dans air-libc-capi::{signal,thread}, ré-export/ancrage air-libc-c, types+prototypes hand-written dans le trailer cbindgen.toml (union struct sigaction, sighandler_t, opaques pthread + initialiseurs, SIG*/SA_*/PTHREAD_*), exclus de la génération cbindgen.

  • Sécurité #1 — 4 fautes INERTES (ADR-064) : sigaction/signal sur SIGSEGV/ SIGBUS/SIGFPE/SIGILL acceptent (retour 0) et mémorisent la disposition (illusion d’API : table process-globale pour oldact) mais n’installent RIEN → la faute prend toujours l’action par défaut (coredump). Les signaux gérables sont installés en async réel (air_signal::install_handler, le kernel appelle le handler C). sigprocmask/pthread_sigmask→masque ; kill/raise→envoi ; sigpendingpending ; sigsuspendreplace_mask + futex_wait interruptible.
  • pthread sur raw_futex (mémoire opaque de l’appelant, jamais AirMutex) : mutex 3 états NORMAL/RECURSIVE/ERRORCHECK (owner = Tid) + timedlock ; cond par compteur de séquence + timedwait (échéance REALTIME/MONOTONIC) ; rwlock lecteur-préférentiel ; once (3 états) ; TLS (key_create+dtor à la sortie de thread via std, hôte). Cycle de vie create/join/detach/self/equal sur le spawn d’air-thread (hôte, pthread_t = Tid). Types opaques ABI Air largeur fixe (mutex 40/cond 48/rwlock 56/once 4, sigset_t 128) + initialiseurs zéro.
  • Additifs REMONTÉS (non inventés) : sortie de thread propre (pthread_exit endort le thread faute de primitive couche 1 + panic=abort), crochet de destruction TLS cible, rt_sigsuspend atomique, kill groupe de processus. Déviations gravées D-M3.1..D-M3.9 (docs/libc-conformance.md).
  • Validation superviseur en attente (cargo/git gated dans l’environnement de production) : gen-capi-header (régénère air_c.h — trailer M3), barrière + nm -D (sigaction/kill/pthread_create/pthread_mutex_lock…), programme C multi-thread, signal non-faute délivré async + faute → coredump, sur x86_64 ET aarch64. Cargo.lock à régénérer (2 nouvelles crates).

2026-06-29 — 🧭 CORRECTION D’ARCHITECTURE : air-rt est un objet couche 1 (ADR-052)

Relecture du layering : le crate rt/crates/air-rt (phase 2) appelle la couche 0 en direct (arch::set_fs, mem::mmap, process::clone_thread, futex, exit_group) — violation de la doctrine d’Air (seule la couche 1 consomme la couche 0 ; personne d’autre). ADR-052 (ratifié BDFL) grave la correction : air-rt est un objet couche 1, AirRuntime, composé de nouvelles briques couche 1 à créer (elles n’existent pas encore) — ThreadControlBlock, ThreadLocalStorage, outils de relocation (cible PIE/static-PIE, ADR-050) — s’appuyant sur air-thread/air-mem/air-proc. AirRuntime ne dépend jamais de air-sys-syscall (la surface kernel) — air-sys-types (types transverses : AirError/Errno, Pid/Tid… ADR-052 D6) reste autorisé. Les wrappers de syscalls (dont set_fs, ADR-051) restent en couche 0 ; leur consommateur devient un objet couche 1 (jamais le runtime). Le tree hors-arbre rt/ ne garde que le point d’entrée ELF _start + la plomberie cible (build-std/nightly). Doc mise à jour / tagué obsolète : ADR-049 D8 (bannière + biffure), ADR-051 (note layering + reformulation « affaire d’air-rt »), macro-architecture (§Couche 1 : objet air-runtime + tableau d’artefacts), family-arch.md/family-process.md, INDEX, rt/README.md/rt/DECISIONS.md, registre (v3.17) + SUMMARY. Gain : la logique de runtime, devenue couche 1, retombe sous le **100 % couverture

  • barrière 2-arches** (le carve-out rt/ ne couvre plus que _start + cible). Code à refactorer (création des objets couche 1, déplacement, retrait dépendance air-sys-syscallair-sys-types conservé) = chantier suivi séparément.

2026-06-28 — 🔒 RE-SCEAU couche-0-v1.7 : 7 syscalls pour le runtime Air et la libc

Descellement unique (ADR-051, ratifié BDFL) ajoutant 7 wrappers typés à la couche 0, pour débloquer la phase 2 du programme *-linux-air (runtime air-rt) et pré-équiper l’objectif libc (ADR-046) — un seul re-sceau plutôt que des cycles répétés. arch_prctl (set_fs/get_fs, x86_64-only, nouvelle famille arch) programme le registre TLS du thread principal x86 (sur aarch64 = tpidr_el0 en asm, affaire d’air-rt ; threads spawnés = CLONE_SETTLS). process : getppid (→ Option<Pid>, 0=parent hors namespace), set_tid_address (primitif de join), sched_yield, umask, getcwd (octets, Principe 3), getrusage (+ types Rusage/RusageWho/Timeval #[repr(C)]). Tous en asm 2-arches avec // SAFETY:. 16 tests dont stratégies sûres pour les cas délicats (arch_prctl : no-op sur la base courante ; set_tid_address : threads détachés + Arc). check-syscalls PASS (numéros validés vs headers uapi, 2 arches), couvrable VIDE (2 exceptions STRUCTURAL/EFAULT-SAFE : bras Err de get_fs/getrusage inatteignables par construction — buffer valide + who typé), audit-exceptions cohérent (77 entrées, STRUCTURAL 46). Reporté en couche-0-v1.8 (phase 5, avec OpenSSH comme consommateur) : termios (ioctl typé), PTY, rt_sigaction/sigaltstack (+ décision ADR-020).

2026-06-28 — 🔒 RE-SCEAU couche-0-v1.6 : couche 0 std-free + futex(2) + types FD natifs + clone3 spawn de thread

Descellement unique de la couche 0 (ADR-048 + Amendement 1, ratifiés BDFL) regroupant : (1) famille futex(2) classique (FUTEX_WAIT/WAKE typés, EINTR remonté ; PR #147) ; (2) internalisation des types FD (OwnedFd/BorrowedFd/RawFd natifs d’Air + close(2) dans air-sys-typescore::os::fd n’existe pas en stable ; PR #148) ; (3) std-free #![cfg_attr(not(test), no_std)] (ffi→alloc/core, Arcalloc, OsStringVec<u8>, std::sync::MutexFutexMutex ; PR #149) ; (4) clone3 spawn de thread (clone_thread + trampoline asm x86_64/aarch64, join CHILD_CLEARTID/futex ; PR #150). Critère de sortie atteint : cargo build -p air-sys-types -p air-sys-syscall --lib compile en #![no_std] (preuve par le compilateur de l’absence de std en code lib). Couverture couvrable VIDE (branches ~84,3 %), // SAFETY: sur chaque unsafe, vérifié sur les 2 arches (bug asm aarch64 x19 réservé LLVM → x21, attrapé par la vérif 2-arches). Débloque la réimpl air-thread std-free (couche 1) sur futex/clone3, prérequis de la libc Air (ADR-046/047). Points à réconcilier par RFC : la spec air-sys-types.md montre encore CloneResult::Child pour les threads (unsound → clone_thread distinct) ; outillage ABI Rust air-abi-check/air-symver non implémenté (ADR-012).

Rôle de ce document

Tableau de bord dynamique du projet. Donne en un coup d’œil :

  • L’état d’avancement courant.
  • Le prochain chantier prioritaire et son contexte.
  • Le backlog priorisé (vue condensée).
  • L’historique des sessions précédentes.

Pour la checklist structurelle exhaustive (tous les documents à produire, arborescence cible, statistiques), voir INDEX.md. Pour le détail des ADRs, voir adrs/registre-adrs-fr.md. Les deux sont à consulter quand on cherche un détail précis ; JOURNAL.md sert à savoir où on en est et ce qu’on fait ensuite.

Convention d’usage en début de session : prompt initial type — « Bonjour, on continue sur Air. Lis docs/JOURNAL.md pour le contexte de reprise, puis on attaque [sujet]. »

État courant

2026-06-25 — 🎯 JALON : air-config COMPLET (5 backends + validation cas-réels). Suite des tranches 1-3 (chaîne verticale), livrés : tranche 4 (backends systemd — symlinks *.wants/mask/drop-ins/default.target, remplacement atomique ; fstab — 6 colonnes déterministes ; PR #122) et tranche 5 (passwd/group/shadow, PR #125 — SÉCURITÉ : shadow créé 0600 dès la naissance via openat2, jamais de chmod après-coup ; hashes en Zeroizing effacés après écriture ; jamais loggés/diagnostiqués ; is_safe_hash anti-injection ; tests comptes synthétiques en tempdir, robustes root). Validation cas-réels (PR #126) : corpus de fixtures /etc réalistes + comparaison sémantique (modèle-vs-modèle, détection de perte) = gate CI reproductible ; harnais advisory /air/etc (examples/etc-fidelity) produisant imported/artifact/reemitted/semantic-diff par backend sur la vraie machine — fidélité sémantique parfaite sur le /etc réel de carbon (138 units, 135 comptes), zéro secret (/etc/shadow jamais lu ; re-scan → 0 hash sous /air/etc, vérifié indépendamment). 5 backends /etc couverts (resolv.conf/hosts, systemd, fstab, passwd/group/shadow). Zéro unsafe/as/dépendance externe/surface C ; 6 cibles fuzz ; 100 % hors exceptions STRUCTURAL ; code généré *_capnp.rs exclu de la couverture (ADR-040). A nécessité le fix couche 0 faccessat2 (re-sceau v1.4, PR #123 — faccessat honorait pas son flag AT_SYMLINK_NOFOLLOW). Reste pour clore la couche 1 : l’ABI C (libair-base.so, doc cadrée ADR-027 addendum).

2026-06-25 — 🎯 JALON : air-config — chaîne verticale complète (tranches 1-3, PR #116/#118/#120). Le modèle de configuration d’Air (ADR-033/040/041) est implémenté de bout en bout, en 3 crates : air-config-compile (parseur du format de source maison — schema-directed, hand-rollé, fuzzé ~1,4 M runs ; binding au schéma + did-you-mean ; conversions typées checked anti-overflow/signe, zéro as ; décodage bytes hex/b64 ; secrets zeroizés/élidés ; encodage Cap’n Proto déterministe → artefact bit-pour-bit reproductible), air-config-schema (4 domaines .capnp : network/services/accounts/mounts + annotations $secret/$domain/$backend ; code Rust généré committé ADR-040 ; build sans tool C++ prouvé ; tool capnp 1.1.0 pinné), air-config (façade : lecteur mmap zéro-copie + vérif magic/checksum/versionInvalidData, jamais de lecture muette ; XDG ; générations + switch atomique + rollback via write_atomic ; trait Backend paramétré par racine + backend resolv.conf/hosts émission swap-atomique + import inverse défensif). Aucune fn unsafe exposée, zéro as, zéro dépendance externe, zéro surface C (capnp runtime pur Rust). 3 cibles fuzz ; 100 % lignes+branches hors exceptions STRUCTURAL ADR-035. Code généré *_capnp.rs exclu de la couverture (--ignore-filename-regex, corollaire ADR-040). Tests backends en tempdir (assert explicite root != /etc). Décisions design : ADR-040 (Cap’n Proto + addendum tool-C++/code-committé), ADR-041 (coexistence /etc), ADR-027 addendum (doc ABI C). Tranche 4 différée (backends systemd → passwd/shadow prudent → fstab). Reste couche 1 : tranche 4 air-config + ABI C (libair-base.so).

2026-06-25 — 🎯 JALON : air-base-lib COMPLET (cœur + services, PR #110). Les services sont livrés : AirLog (journalisation journald via protocole socket natif AF_UNIX, zéro dépendance systemd ; petites entrées CLÉ=valeur / forme binaire ; repli memfd scellé + SCM_RIGHTS sur EMSGSIZE ; non-fatallog() n’échoue jamais, pertes comptées) + identifiants (AirUuid v7/v4, AirId128 via /etc/machine-id, AirMonotonicId). Aucune fn unsafe exposée, zéro as, zéro dépendance externe, zéro surface C ; 94 unit + property + 11 doctests + 2 cibles fuzz (journal_encode, uuid_parse) ; 100 % hors exceptions STRUCTURAL in-code (bras d’erreur kernel inatteignables : add_seals/write memfd, getrandom partiel ≤256 o). Cœur non régressé (byte-identique). tracing-bridge différé (zéro dépendance). §3 Config NON implémenté ici : superseded par ADR-033/040/041 → crate dédiée air-config (compilateur + artefact Cap’n Proto). Couche 1 — état : 8/8 crates cœur + air-base-lib services ✓. Reste pour clore la couche 1 : air-config (chantier dédié) et l’ABI C (libair-base.so, différée).

2026-06-25 — 🎯🎯 JALON MAJEUR : COUCHE 1 — 8/8 CRATES CŒUR IMPLÉMENTÉ (air-process livré, PR #105). Le cœur de la couche 1 est complet : air-base-lib cœur + air-crypto + air-socket + air-filesystem + air-memory + air-device + air-thread + air-process. air-process : AirCommand/AirProcess (clone3+CLONE_PIDFD, redirections dup3, chdir, execve_prepared, wait synchrone waitid-sur-pidfd, signal/kill), AirPipe (pipe2+CLOEXEC), AirExitStatus typé ; drop_privileges (process courant) + drop_privileges_to (drop dans l’enfant post-fork avant exec) — chemin enfant alloc-free (CStrArray + buffer de groupes construits avant clone3, drop_privileges_with_buffer sans alloc → async-signal-safety), ordre sûr no_new_privs→setgroups→setresgid→setresuid→capset→ambient→seccomp→landlock→verify, codes d’échec distincts spawn/drop. Aucune fn unsafe exposée, zéro as. Tests d’intégration réels + 4 tests root (regain de privilège irréversible, accès root révoqué via drop_privileges_to) + Miri ; 100 % lignes+branches hors exceptions ADR-035 in-code (CHILD-EXIT du chemin enfant, STRUCTURAL wait/clone3). A nécessité une extension couche 0 : execve_prepared (exec sans allocation pour le chemin post-fork) — additive, re-scellée couche-0-v1.3. Décisions config gravées en parallèle : ADR-040 (artefact air-config = Cap’n Proto ; addendum : capnpc exige le tool C++ au build → politique code généré committé) + ADR-041 (coexistence /etc : projection générée, RO sélectif, air-config seul écrivain). Reste pour clore la couche 1 : air-base-lib services (logging journald natif + identifiants ; air-config = chantier dédié Cap’n Proto) et l’ABI C (libair-base.so, différée).

2026-06-25 — 🎯 JALON : air-thread livré (PR #97) — threads + synchronisation de la couche 1. AirThreadBuilder (nom, taille de stack, affinité CPU — implémentée via la couche 0 set_cpu_affinity+gettid, handshake par canaux : l’enfant publie son Tid, l’appelant pose l’affinité avant de débloquer le corps) + AirThreadHandle<T> (joinAirResult). AirMutex/AirRwLock/AirSemaphore au-dessus de std::sync, SANS poisoning (poison absorbé via into_inner, testé par empoisonnement délibéré). AirChannel MPSC ; atomics ré-exportés. Aucune fonction unsafe exposée, zéro as, arithmétique saturante. Property-based + Miri (35 tests ; 5 ignorés = affinité en asm + stack, non supportés par Miri). 100 % lignes+branches hors 1 exception STRUCTURAL (usize::try_from(cpu) Err, impossible sur cibles 64 bits, doc in-code). Backend std::sync v1, migration futex maison différée. NB : l’affinité CPU était marquée « différée » dans la spec ; implémentée car la couche 0 (PR #55) la fournit → spec mise à jour. Piloté par agent headless sur carbon. Couche 1 : 7/8 crates cœur implémenté (+ air-thread).

2026-06-24 — 🎯 JALON : air-device livré (PR #94) — énumération/surveillance/identité natives de la couche 1. Natif sysfs + netlink uevent (groupe KERNEL) + evdev, sans libudev/libc. §1–§4 : AirDevice (attributs/propriétés/parent), AirDeviceEnumerator (/sys/class+/sys/bus, dédup canonique, filtres), AirDeviceMonitor (hotplug uevent), typés AirNetDevice/AirBlockDevice/AirUsbDevice. §5 AirInputDevice (evdev : capacités bitmap, horloge monotone EVIOCSCLOCKID, AirInputEvent::from_raw pur). Monitor durci : next_event couvert par test d’intégration root réel (uevent synthétique echo change > /sys/.../uevent) — plus d’exception PRIVILEGE. Aucune fonction unsafe exposée, zéro as, parsing borné get(). 3 cibles cargo-fuzz (uevent/sysfs/input ; input 5,5 M runs, 0 crash). Production 100 % lignes+branches hors 1 exception STRUCTURAL (write_attribute partiel, écriture sysfs atomique ADR-035) ; résidus = code de test (ADR-035 TEST-HARNESS). Ajout couche 1 : air-base-lib::AirInstant::from_monotonic_micros (horodatage evdev ; air-base-lib reste 100 %). Piloté par agents Claude Code headless sur speedy (gate = barrière x86 speedy+carbon + CI ARM). Couche 1 : 6/8 crates cœur implémenté (air-base-lib cœur + air-crypto + air-socket + air-filesystem + air-memory + air-device).

2026-06-24 — 🎯 JALON : air-memory livré (PR #88, par carbon) — allocateurs spécialisés + comptabilité de la couche 1. 5 sections : AirMemoryTracker/AirMemoryUsage (comptabilité per-composant atomique, opt-in, peak monotone via fetch_max) ; AirArena (bump O(1), alignement align_offset/checked_*, contrat gravé : reset/Drop n’exécutent PAS le Drop des valeurs, OOM par try_reserve sans abort, unique unsafe interne avec // SAFETY: par bloc) ; AirObjectPool/AirPooled (Rc/RefCell, croissance sans plafond, RAII respecte Drop) ; AirSlab/AirSlabKey (slots stables anti-ABA, génération wrapping_add u32, zéro unsafe) ; AirBacking (heap ; mmap différé, API non-cassante). Mono-thread (&mut self), aucune fonction unsafe exposée, arithmétique défensive (zéro as). Zéro dépendance externe, ZÉRO surface C. Tests unitaires + property-based + Miri (incl. test concurrent 8 threads sur le tracker → zéro UB/data-race). Couverture 100 % lignes+branches hors 1 exception STRUCTURAL (bras None du take() dans Drop for AirPooled, inatteignable par construction, gardé car un Drop ne doit pas paniquer ; doc in-code ADR-035). Décisions ratifiées : slab wrapping_add, pool sans plafond, peak monotone. 1ère crate couche 1 entièrement pilotée par agents Claude Code headless sur carbon (orchestration multi-machines ; gate de merge = barrière x86 speedy+carbon + Miri + CI ARM, ADR-037). Couche 1 : 5/8 crates cœur implémenté (air-base-lib cœur + air-crypto + air-socket + air-filesystem + air-memory).

2026-06-16 — 🎯 JALON : air-filesystem livré (PR #87, par carbon) — opérations filesystem de la couche 1. 6 sections sur la couche 0 (famille fs + fs::inotify, types air-sys-types::fs) : chemins canoniques & confinement resolve_within (anti-path-traversal par les flags kernel RESOLVE_BENEATH/RESOLVE_NO_MAGICLINKS d’openat2, sécurité par construction) ; écriture atomique (tmpfile + fsync fichier + parent + rename) ; AirTempDir RAII (Drop récursif non-paniquant, symlink-safe) ; copie copy_file_range + repli read/write transparent, copy_tree borné (anti-boucle de symlink) ; watchers sur inotify (debounce, récursivité, corrélation move par cookie, Q_OVERFLOW → variante dédiée Overflow = zéro-perte ADR-032) ; recherche bornée (read_dir, glob maison sans dépendance, find_regex différé). Zéro dépendance externe ajoutée, ZÉRO surface C (gate check-c-surface vert en défaut et --all-features). Correction de contrat : la note spec « inotify à ajouter en couche 0 » était périmée (inotify y est ; décodage inotify_event déjà fait/fuzzé en couche 0 → couche 1 consomme des événements décodés, ne re-parse pas). Tests unitaires + property-based (glob vs oracle, bornage, idempotence) + intégration (confinement, atomicité, copie, watcher inotify réel) + 2 cibles cargo-fuzz (fuzz_glob_match, fuzz_watch_mapping ; 0 crash). Couvrable VIDE (exclusion couverture retirée → air-filesystem mesuré ; lignes ~97 %, les rares bras non couverts sont des gardes défensives STRUCTUREL/ENV documentées in-code, ADR-035). Décision déférée (à acter) : regex (feature search-regex) refusé par la règle des 80 % → find_regex renvoie Unsupported (glob couvre le courant) ; exception nommée vs report définitif laissé à Thierry. Couche 1 : 4/8 crates cœur implémenté (air-base-lib cœur + air-crypto + air-socket + air-filesystem).

2026-06-16 — 🎯 JALON : air-socket livré (PASSE 2, PR #86) — sockets typés + résolution enfichable + client DNS maison. Sockets synchrones sûrs sur la famille net couche 0 : TCP (AirTcpSocket/Listener, connect_to_host IPv6-first), UDP (AirUdpSocket), Unix stream/listener/datagram + passage de FD SCM_RIGHTS (capability AirCom, ADR-001). OwnedFd RAII, as_fd() seam couche 2, zéro unsafe exposé. Résolution maison enfichable et ordonnée (AirNameResolver, sources [hosts, DNS], IPv6-first/IPv4-fallback) — sans libc/NSS/getaddrinfo. Client DNS maison RFC 1035 (AirDnsSource, A+AAAA, UDP + repli TCP sur bit TC, timeout 5 s/2 essais via poll MSG_DONTWAIT+clock_nanosleep). Parseur DNS = surface de sécurité (Principe 3) : slicing borné get(), pas de suivi de pointeur de compression (anti-boucle par construction), checked_* ; fuzz cargo-fuzz obligatoire (fuzz_dns_parse, smoke 300 000 exécutions, 0 crash). Tests unitaires + property + intégration loopback (TCP/UDP, Unix + passage de FD, serveur DNS en-process + repli TCP). AirNetlinkSocket RETIRÉ (décision 2026-06-15) : la couche 0 scellée n’a pas de socket netlink générique → futur crate air-netlink (couche 2, RFC netlink couche 0 requis). Zéro dépendance externe, zéro surface C (check-c-surface vert). Couche 1 : 3/8 crates cœur implémenté (air-base-lib cœur + air-crypto + air-socket).

2026-06-15 — 🎯 JALON : air-crypto livré (PR #80, merge f7057dd) — cœur crypto moderne de la couche 1. 7 sections (random/keys/hash/hmac/aead/kdf/asymmetric) sur RustCrypto/dalek, purs Rust, ZÉRO surface C (gate check-c-surface vert, ban cc respecté). XChaCha20Poly1305 recommandé par défaut (nonce 192 bits, anti-mésusage) ; BLAKE3 différé (blake3 tire cc → conflit zéro-C ; AirHashAlgorithm #[non_exhaustive]) ; streaming AEAD différé (frontière de volume documentée). KAT de conformité officiels (RFC 8439/8032/9106, NIST CAVP, RFC 4231/5869/7748) traversant le câblage de prod (seams pub(crate)) ; property-based ; zeroize-au-Drop vérifié ; fuzz×4. Couvrable VIDE (exclusion couverture retirée → air-crypto mesuré, planchers tenus). Exception crypto nommée dans EXCEPTIONS.md (ADR-034/ADR-016). Couche 1 : 2/8 crates cœur implémenté (air-base-lib cœur + air-crypto).

2026-06-15 — air-base-lib : module encoding (base64/hex) livré (PR #83, merge 9474c8b). Base64 (RFC 4648) + hexadécimal, std-only, zéro dépendance, zéro surface C ; 100 % lignes/branches (encoding.rs), property-based + cibles cargo-fuzz de décodage. Surface validée par Thierry ; livré complet d’un coup (squelette jamais mergé seul) → air-base-lib reste à 100 % sur main. 1ᵉʳ crate produit par la machine carbon (relais multi-machines).

2026-06-15 — 🎯 JALON : 1ᵉʳ crate couche 1 implémenté (air-base-lib cœur, PR #73, merge cb480ac). Erreurs / chaînes-chemins / temps, adossés à icu4x 2.2 (compiled_data, données baked-in). 100 % couvrable VIDE : lignes/branches 100 % du code atteignable ; les 2 seuls bras non couverts — erreur des horloges kernel (AirInstant::now/AirDateTime::now_utc) — sont STRUCTURELLEMENT inatteignables (ADR-035), documentés in-code, sans seam d’injection (cohérent avec la politique clock_settime couche 0). 4 cibles cargo-fuzz (from_utf8/normalize/path/locale), property-based (proptest), doctests exécutables. Décisions calendrier validées par Thierry : en couche 1 (sans formatage), le japonais partage la numérotation grégorienne CE et l’hébraïque un mois ordinal ; l’ère (Reiwa…) et le MonthCode (Adar I/II) relèvent du formatage (couche 2). deny.toml admet Unicode-3.0 (icu4x 2.x a quitté Unicode-DFS-2016). Dette d’outillage consignée (à NE PAS résoudre ici) : les gates xtask couvrable-vide/audit-exceptions sont câblés couche 0 (filtre air-sys-*) ; leur extension à la couche 1 se heurte à une collision de basename (time.rs existe en couche 0 ET en couche 1) + recompute du récap ⇒ RFC/tâche séparée, les exceptions couche 1 restant documentées in-code en attendant. Couche 1 : 1/8 crates cœur implémenté (air-base-lib cœur ✓ ; services à venir).

2026-06-15 — 🔒 Politique « zéro surface C/C++ » VERROUILLÉE au gate (PR #81, merge 4544709 ; issue de l’audit 082). Deux verrous : deny.toml [bans] (ban nominatif cc/cmake/bindgen/pkg-config/aws-lc-sys/ring) + cargo xtask check-c-surface (garde-fou générique anti-*-sys, parseur testé, scope cargo tree -e normal,build), câblé en CI supply-chain et dans cargo xtask barrier. Produit vérifié zéro-C (48 crates, 0 *-sys). Faux positif neutralisé : deny.toml [graph] exclude = ["loom"] retire le sous-arbre cfg(loom) → generator → cc (harnais de modèle de concurrence test-only, déjà exempté dans machete) — le ban cc reste actif sur tout autre chemin. Réintroduction future (ex. ring pour air-tls) = exception NOMMÉE dans docs/EXCEPTIONS.md (ADR-024/034).

Date de dernière mise à jour : 2026-06-14 — 🔒 COUCHE 0 v1 — SCELLÉE. Audit de barrière complète vert sur tout le workspace, 2 arches (x86_64 speedy, aarch64 raspi-srv-2) : cargo fmt --check (stable et nightly), clippy --workspace --all-targets -D warnings (0 warning), cargo test --workspace (918 tests, 0 échec), cargo llvm-cov en root (lignes 97,73 % / branches 82,73 % — chaque rouge ⊆ une exception documentée de COVERAGE-EXCEPTIONS.md ; DEFERRED-TOOLING VIDE → couvrable VIDE au sens strict), cargo audit + cargo deny check + cargo machete verts, // SAFETY: sur CHAQUE bloc unsafe du workspace (lacune time.rs corrigée — PR #64), mdbook build FR+EN sans lien cassé. CODE COMPLET : 11 familles (process/fs/mem/signal/time/net/ipc/security/system/device/ebpf) + io_uring 12 Temps (1, 2a–2d, 3a–3f, 4) + extensions (MmapRegion, privsep, fs::inotify, affinité CPU). Doc développeur FR + EN. Tag couche-0-v1 (annoté, signé GPG). Caveat de sceau — RÉSORBÉ post-sceau (PR #66 mergée, merge 380d389, prompts 060/061) : les gates qualité sont gravés en cargo xtask (5 gates : barrier, couvrable-vide, check-syscalls, audit-exceptions, repro — dev-tooling zéro-dépendance, exclu de la couverture). La reproductibilité ADR-025 est désormais outillée (cargo xtask repro : double-build en env contrôlé, 4 rlibs bit-pour-bit) ; la dette de re-sync des fichier:ligne est outillée (cargo xtask audit-exceptions : format/taxonomie ADR-035 + recompte du récap, échec sur dérive). Ce gate a corrigé une dérive du décompte : registre à 69 entrées production (STRUCTURAL 38, FEATURE-KERNEL 13, PRIVILEGE 10, CHILD-EXIT 8, DEFERRED-TOOLING 0) — CHILD-EXIT 9/total 70 était un sur-comptage introduit au 7b00e75 (055), pas une rangée perdue (décision Thierry, 2026-06-14). cargo xtask check-syscalls certifie par ailleurs 161 numéros de syscalls conformes aux headers uapi des 2 arches (0 écart). Différés assumés (non bloquants) : types epoll spéculatifs (revue fin couche 3), futex synchrone (v2 own-futex air-thread), fanotify, différés io_uring 3a (submit_files_update/msg_ring_fd). Prochaine phase : implémentation couche 1 (air-base-lib cœur d’abord).

Historique — état précédent (2026-06-14, avant sceau)

Taxonomie d’exceptions actée (ADR-035) + 3 candidates COUVERTES — PR #60 mergée (aa47d74) : fs::try_lock, net::get_so_error, process::pidfd_send_signal(Some)DEFERRED-TOOLING VIDE / couvrable VIDE au sens strict ; registre recompté (70 entrées production : STRUCTURAL 38, FEATURE-KERNEL 13, PRIVILEGE 10, CHILD-EXIT 9 ; TEST-HARNESS en annexe — CHILD-EXIT 9/total 70 corrigés en 8/69 au 061, sur-comptage débusqué par xtask audit-exceptions) ; ADR-035 companion d’ADR-031. Puis : intro développeur couche 0 (PR #62, reflet Q4 family-security), traduction anglaise couche 0 (18 docs, PR #63).

  • Phase : design pré-ouverture publique. Implémentation couche 0 démarrée dans un workspace Rust privé.
  • Règle de pilotage actée le 2026-05-27 : le dépôt reste privé tant que la couche 0 n’est pas réellement opérationnelle ; tout travail d’ouverture publique ou de polish associé reste secondaire tant que cet objectif n’est pas atteint.
  • Documents fondateurs (vision, charte, principes d’ingénierie) : finalisés en v1.0.
  • ADRs fondateurs + ADR-018 imagerie : 18 ADRs (001 à 018, plus ADR-012-bis) finalisés et figés.
  • ADRs phase 0 (couche 0, runtime, infrastructure) : série complète, 7 ADRs finalisés (ADR-019 à ADR-025). La zone « design phase 0 » est close.
  • Implémentation couche 0 : deux crates privées déjà présentes dans le workspace : air-sys-types (types fondamentaux) et air-sys-syscall (wrappers syscall). Les 9 familles de syscalls sont implémentées ET couvertes à 100 % (lignes + branches) hors exceptions documentées, validées sur x86_64 (speedy) ET aarch64 (raspi-srv-2) au 2026-06-01 : process, signal, time, ipc, mem, fs, net, security, system + air-sys-types. Suite : 371 (air-sys-syscall) + 80 (air-sys-types) + 74 doctests + 2 + 1 intégration, 0 échec, sur les 2 arches. Reste couche 0 : io_uring, familles device/ebpf à spécifier.
  • Toolchain Rust : pin exacte relevée à 1.96.0 (bump depuis 1.95.0 le 2026-05-31 ; le minimum requis suit toujours la dernière stable utilisée), avec composants rustfmt, clippy, rust-src, llvm-tools-preview et targets Linux x86_64-unknown-linux-gnu + aarch64-unknown-linux-gnu. Motivation : conserver un dépôt Linux-first sur des versions stables récentes, débloquer l’outillage moderne (cargo-deny, cargo-audit, cargo-machete) et bénéficier du linker rust-lld par défaut sur x86_64-unknown-linux-gnu.
  • Specs couche 0 : toutes les familles sont spécifiées (2026-06-11). 11 familles (process, fs, mem, signal, time, net, ipc, security, system, device, ebpf) + io_uring (maître + 13 Temps, FR+EN) + air-sys-types (partiel). Les deux derniers trous — device (uevent/evdev, parsers zéro-alloc, sysfs→fs) et ebpf (bpf() exhaustif 37 cmd + perf_event_open) — sont comblés. Reste à produire : la spec exhaustive air-sys-types (type par type), en parallèle de l’implémentation. Socle de bindings tranché : syscalls directs via core::arch::asm!, sans dépendance externe (pas de rustix).
  • Documents de setup (phases A à D) : finalisés.
  • Macro-architecture : passe 1 (nettoyage) appliquée le 2026-05-20. Section 2 (couche 0) alignée sur les ADRs phase 0 (019-022) et sur le découpage en deux crates air-sys-types + air-sys-syscall. Toutes les références prospectives ADR-018 à ADR-026 neutralisées en marqueurs neutres. Passe 2 (complétion sections couches 1-5) à mener plus tard en parallèle de l’implémentation.
  • Documentation infrastructure : README.md, README-fr.md, LICENSE, .gitignore présents à la racine ; toute la doc (dont docs/EXCEPTIONS.md, docs/COVERAGE-EXCEPTIONS.md, docs/JOURNAL.md) est consolidée sous docs/ (hygiène documentaire 2026-06-01). Restent à produire : CONTRIBUTING.md, SECURITY.md, CODE_OF_CONDUCT.md, CHANGELOG.md et les guides/conventions dérivés.
  • Dépôt GitHub : air-desktop-project/air, privé, créé le 2026-05-20. Premier commit b3737d6 signé GPG (clé A9F56C4D9F59EE03), DCO Signed-off-by, verified par GitHub (reason: valid). Branche main poussée sur origin/main. Compte GitHub fondateur : tdelhaise.
  • Durcissement CI — PR #10 mergée le 2026-06-02 (3f1034e sur main). Corrige le flaky des 3 tests pidfd_* (défaut d’observation de test, pas une course de production : le numéro de fd fermé était réutilisé par un autre thread avant l’observation EBADF). Correctif côté test, API gelée : double défense par construction — dup vers un fd haut (min(soft NOFILE−1, 4096), non réutilisable par les opens concurrents qui prennent le plus petit fd libre) + FD_OBSERVATION_LOCK sérialisant les 3 frères. Plus purge .profraw (cargo llvm-cov clean) avant chaque mesure de couverture et doc du bruit landlock bénin (docs/CI.md). Couverture inchangée (96.61 % lignes / 78.79 % branches — baseline avec exceptions). 3 runs CI verts consécutifs sur les 2 arches. Réserve notée : l’immunité du fd haut tient tant que la suite ne dépasse pas le repère haut — à revisiter si un test ouvre des milliers de fd concurrents.
  • Incident opérationnel raspi-srv-2 — détecté puis RÉSOLU (2026-06-02). Pendant PR #10, raspi ne pouvait plus git fetch (remote inaccessible) : machine figée sur un vieux SHA (202e1d9, sans fs.rs), reset --hard origin/main en échec silencieux, validation initiale sur code périmé (faux « vert », rattrapé via un E0432) ; contournée alors par rsync. Réparé par Thierry : remote SSH + clé de déploiement dédiée — git remote set-url origin git@github.com:air-desktop-project/air.git puis git config core.sshCommand "ssh -i ~/.ssh/air_deploy -o IdentitiesOnly=yes". git fetch fonctionne. Réflexe conservé : après chaque délégation, vérifier le SHA réellement bâti — au moment du fix raspi était encore à c01f3b8 (PR #9) et doit re-fetch pour atteindre 3f1034e (PR #10).
  • Renommage ADR-029 — PR #11 mergée le 2026-06-02 (d1ce8ad sur main). Types façade + paramètres des crates couche 0 dé-abrégés (refactor mécanique, comportement préservé, couverture maintenue, barrière verte 2 arches). Suite : champs publics façade (PR #12), puis io_uring Temps 1. Cette session lande la spec io_uring (maître + Temps 1–4, FR+EN, ADR-028/029) jusque-là non commitée.

Structure physique du dépôt : racine = README.md, README-fr.md, LICENSE, CLAUDE.md, AGENTS.md, .gitignore, Cargo.toml, Cargo.lock, rust-toolchain.toml, docs/, crates/, target/. Toute la documentation vit sous docs/ (y compris EXCEPTIONS.md et COVERAGE-EXCEPTIONS.md, déplacés le 2026-06-01) ; tous les ADRs vivent dans docs/adrs/. Les documents de cadrage et le workspace couche 0 cohabitent dans le même dépôt privé.

Prochain chantier

Implémentation couche 1 — air-base-lib (cœur) d’abord.

🔒 La couche 0 est SCELLÉE (v1, 2026-06-14, tag couche-0-v1). CODE complet (11 familles + io_uring 12 Temps + extensions), barrière complète verte sur les 2 arches, 100 % de couverture hors exceptions documentées (couvrable VIDE au sens strict, ADR-035), doc développeur FR + EN. Le socle syscall n’évolue plus que par RFC (ADR-015). La phase couche 0 est close.

Prochain chantier : démarrer la couche 1, en commençant par le cœur de air-base-lib (erreurs, chaînes/chemins, temps — cf. specs/layer-1/air-base-lib-core.md), au-dessus du socle couche 0 gelé.

En parallèle / hors phase : outillage xtask qualité (dont xtask audit-exceptions pour re-synchroniser les renvois fichier:ligne du registre de couverture — dette ouverte au sceau), et la spec exhaustive air-sys-types type par type si besoin pour la couche 1.

Différés assumés de couche 0 (non bloquants, revus en temps voulu) : types epoll spéculatifs (revue fin couche 3), futex synchrone (v2 own-futex air-thread), fanotify, différés io_uring 3a (submit_files_update/ msg_ring_fd).

Backlog priorisé

Court terme (avant ouverture publique)

  1. Famille ipc. Prochain chantier (cf. ci-dessus).
  2. Famille mem minimale utile. Priorité suivante : mmap_anonymous, mmap_file, munmap, mprotect, memfd_create.
  3. Famille fs minimale. openat2/openat, read, write, pread, pwrite, statx, lseek, fsync, fdatasync.
  4. io_uring — Temps 1. À démarrer une fois les familles synchrones minimales en place, conformément à ADR-022.
  5. Documents d’ouverture publique (CONTRIBUTING.md, SECURITY.md, CODE_OF_CONDUCT.md, CHANGELOG.md) : explicitement secondaires tant que la couche 0 n’est pas réellement opérationnelle.

Moyen terme (en parallèle de l’implémentation)

  1. Révision architecture/macro-architecture-fr.md — passe 2 (complétion). Enrichir sections couches 1 à 5 avec les détails issus des ADRs et specs phase 0+ une fois la couche 1 entamée. La passe 1 (nettoyage) est faite ; la passe 2 reste à mener.
  2. Spec détaillée du module io_uring (9 documents). Voir backlog INDEX.md. À produire au niveau de détail des autres specs de familles. Réf. ADR-022 + specs/layer-0/io-uring-overview.md.
  3. Spec exhaustive de air-sys-types. Compléter specs/layer-0/air-sys-types.md type par type (méthodes, invariants, tests).
  4. Documents communauté : docs/guides/first-contribution.md, docs/guides/getting-started/.

Plus tard

  1. Documents internes mainteneurs : docs/maintainers/release-process.md, moderation.md, onboarding-maintainers.md, cve-handling.md.
  2. ADRs services système couche 5 à produire : screencapture, appearance/theming, share, firewalld, power, printd/bluetooth/nfc, prefs, keychain. Numérotation à assigner au moment de l’instruction (≥ ADR-026, sans pré-réservation).
  3. ADRs ouverts identifiés dans le registre : logging/observabilité au-delà de journald, mise à jour atomique du système, App Store Air, protocole privé AirCom air-wm ↔ apps natives, politique d’accès des agents IA à l’arbre sémantique et aux captures vectorielles.

Dettes connues

  • Protections de branche GitHub non actives. Le tier gratuit GitHub Free bloque l’API de protection de branche sur les repos privés d’organisation. Conséquence : actuellement, aucune protection contre force-push ou suppression de branche sur main ; aucune exigence d’enforcement côté GitHub des signatures (la signature reste appliquée localement par discipline et vérifiée à l’affichage). Mitigations actuelles : Thierry seul mainteneur, tous les commits signés GPG + DCO par convention. Résolution prévue : soit passage en GitHub Team payant si jugé utile avant l’ouverture publique, soit attente automatique de l’ouverture publique (le repo public sur Free débloque les protections). À réévaluer quand le projet approchera de l’ouverture publique ou s’il commence à avoir des collaborateurs externes.
  • Spécifications phase 0 — COMBLÉES (2026-06-11). Les deux dernières familles couche 0 manquantes sont désormais spécifiées au niveau des autres :
    • Famille device : docs/specs/layer-0/family-device.md — socket netlink uevent + itérateur clé=valeur zéro-alloc, evdev (ioctls dédiés EVIOC* + lecture typée input_event), sysfs renvoyé à fs. Consommée par air-wm/air-console (ADR-007).
    • Famille ebpf : docs/specs/layer-0/family-ebpf.md — bpf() exhaustif (37 sous-commandes) + perf_event_open/ioctls, RAII, charge des programmes déjà assemblés (logique → couche 1).
    • Reste : l’implémentation de ces deux familles (specs prêtes à pousser). Traductions EN à produire en passe ultérieure (comme io_uring).
  • Socle de bindings — TRANCHÉ (2026-05-31, dette résolue). La couche 0 appelle les syscalls directement via core::arch::asm!, sans dépendance externe (pas de rustix). rustix avait été évoqué un temps comme hypothèse de travail dans la macro-architecture early-stage ; la piste est abandonnée. Trace historique conservée dans macro-architecture-fr.md (section « Socle de bindings »).
  • Cohérence macro-architecture passe 2. La passe 1 a aligné la section 2 (couche 0). Les sections couches 1 à 5 contiennent encore des détails datant de la rédaction early-stage qui ne sont plus précis par rapport aux ADRs et specs ultérieurs (par ex. nomenclature AirEvent vs runtime async ADR-023, articulation air-aircom côté couche 2 vs io_uring, etc.). À traiter dans une passe 2 menée en parallèle de l’implémentation couche 1, quand le détail concret existera.
  • madvise io_uring + futex (différés de 2a/2c) — RÉSOLUS (2026-06-12, PR #31). submit_madvise (OP 25) et submit_futex_{wait,wake,waitv} (OP 51/52/53) exigeaient un handle de vivacité partagé sur le mapping (slot S1 empêchant munmap tant qu’une op est en vol). C’est désormais MmapRegion/MmapRegionLiveness (air-sys-syscall::mem, refcount Arc, prouvé Miri+loom) — Mapping reste inchangé. Les deux ops sont branchées ; nommage Mapping/MmapRegion et signature futex (&MmapRegion+offset) réconciliés.
  • Hygiène doc couverture Temps 1 io_uring. Le diagnostic 2a (rapport 017) a relevé ~5 branches/lignes STRUCTURAL préexistantes (non introduites par 2a) du cœur Temps 1 non encore tabulées individuellement dans COVERAGE-EXCEPTIONS.md : gardes ok_or(EINVAL)? de build (i32::try_from(fd), NonZeroU32::new(cq_entries)) et de ring_sizes (checked_add/checked_mul de débordement de tailles d’anneau, fuzzées via ring_sizes_decode), et le bras None de leak_forget. Toutes inatteignables par construction (valeurs kernel jamais hors-borne). Tolérées par le plancher CI 96/78 ; à tabuler explicitement dans une passe d’hygiène doc (orthogonale à la livraison 2a).
  • Additions couche 0 découvertes en spécifiant les couches supérieures (2026-06-11, étendu 2026-06-12) — la méthode « doc-d’abord » paie : spécifier la couche 1 révèle des primitifs couche 0 manquants, désormais spécifiés (specs landées) et à implémenter :
    • fs::inotify (docs/specs/layer-0/family-fs-inotify.md) — inotify_init1/add_watch/rm_watch + décodage zéro-alloc des inotify_event. Prérequis de AirFileSystemWatcher (air-filesystem, couche 1).
    • process privsep (docs/specs/layer-0/family-process-privsep.md) — IMPLÉMENTÉ (2026-06-12, PR #34) : setgroups/setresgid/setresuid + getresgid/getresuid + types Uid/Gid/ResUid/ResGid. Test de sécurité vert (enfant forké : après réduction vers nobody, regain root/gid → EPERM, réajout de groupe → EPERM ; saved-set figé). Couverture 100 % hors exceptions, 2 arches. Débloque air-process::drop_privileges. (Numéro setresgid ARM64 corrigé 143→149 ; constat : Uid/Gid et setuid/setgid n’existaient pas — types créés par cette PR.)
    • MmapRegion (docs/specs/layer-0/family-mem-mmap-region.md) — IMPLÉMENTÉ (2026-06-12, PR #31) : mapping partageable refcounté + garde de vivacité (Miri+loom). A débloqué et branché submit_madvise (2a) ET submit_futex_* (2c) ; signature futex et nommage Mapping/MmapRegion réconciliés. (Reste à implémenter dans cette liste : fs::inotify et epoll.)
    • Famille epoll (découvert au Temps 2c) — EpollOp/EpollEvent ont été ajoutés à air-sys-types::io_uring (consommés par la façade submit_epoll_ctl), mais aucun wrapper epoll_create1 n’existe en couche 0 : l’epfd des tests 2c est créé par un epoll_create1 brut test-only. Si d’autres consommateurs apparaissent, produire une petite famille epoll (epoll_create1 + ré-héberger EpollOp/EpollEvent) — à côté de fs::inotify et MmapRegion.
  • Réconciliation macro-architecture (passe 2) — points précisés (2026-06-11). En complément de la dette « cohérence macro-architecture passe 2 » ci-dessus :
    • AirLog / AirId128 (couche 1) n’utilisent PAS sd-journal/sd-id128 (couche 2) : socket natif journald (/run/systemd/journal/socket) et lecture directe de /etc/machine-id. Corriger les mentions « via sd-journal/sd-id128 » de macro-architecture-fr.md (≈ l.311, 313, 624, 625, 1547).
    • Renommage crates couche 1 (ADR-029, explicite, sans abréviation) : air-fsair-filesystem, air-procair-process, air-memair-memory. Mettre à jour la liste de crates de macro-architecture-fr.md. Les familles couche 0 (fs/mem/net/ipc) restent inchangées (acté).
  • Couche 1 — specs COMPLÈTES 8/8 (2026-06-11 → 2026-06-12). air-base-lib (cœur + services), air-filesystem, air-memory (allocateurs arena/pool/slab + comptabilité per-composant ; mono-thread ; backing heap enfichable), air-process (synchrone, pidfd, drop_privileges = orchestration + politiques fournies), air-thread (threads + synchronisation, backend std::sync v1), air-socket (sockets + résolution de noms enfichable, client DNS maison), air-crypto (primitives RustCrypto) et air-device (énumération/surveillance/identité natives sysfs+uevent) spécifiés (API Rust ; ABI C différée, cf. ADR-027). Les 8 crates de la couche 1 sont désormais spécifiées. Specs landées sous docs/specs/layer-1/. Restent hors crates : le compilateur seccomp déclaratif et le helper Landlock→chemins (côté air-filesystem). Implémentation à venir (après les Temps io_uring restants).
  • ADR-033 acté (modèle de configuration, 2026-06-12). Source typée canonique → compilateur validant (barre couches 0/1, fuzzé) → artefact binaire reproductible schema-first (version + checksum) consommé par le runtime ; projections texte JSON/YAML/TOML en import/export ; sûreté par générations + switch atomique + rollback ; backends de compilation enfichables = joint d’indépendance (unit files systemd générés et validés pour V1, jamais une dépendance dure). L’intégrité des binaires est explicitement hors périmètre → renvoyée à un futur ADR (dette identifiée).
  • air-device natif — dette ADR-005 à amender (2026-06-12). air-device décide énumération/surveillance/identité natives (sysfs + uevent kernel), zéro libudev/udevd ; surface en vocabulaire Air (joint), evdev au niveau périphérique (interprétation → compositeur, hors périmètre), 3 sous-systèmes typés (net/block/usb) + générique. Dette : la ligne « libudev en couche 1 » d’ADR-005 est à amender (pré-public : édition directe ou RFC ADR-015) ; la règle du joint (« vocabulaire Air, vérif CI no-systemd-types ») reste à outiller plus tard.
  • air-thread — décisions gravées + 2 nouveaux manques couche 0 (2026-06-12). Le backend de synchronisation (AirMutex/RwLock/Semaphore, sans poisoning) est std::sync par expédient v1 ; migration planifiée vers une implémentation futex maison (« on maîtrise ce qui s’y fait ; std a ses propres contraintes, pas les nôtres »). Cela révèle 2 primitifs couche 0 manquants (à côté de fs::inotify et epoll) : sched_setaffinity (prérequis de AirThreadBuilder::cpu_affinity) et un futex synchrone (futex(2) WAIT/WAKE direct — prérequis de l’implémentation futex maison). (Distinct du futex io_uring déjà livré au 2c : ici un wrapper syscall synchrone classique.)
  • air-socket — client DNS maison à fuzzer (2026-06-12). Décision (override macro-archi) : résolution de noms enfichable et ordonnée (AirNameResolutionSource/AirNameResolver, AirHostsFileSource, client DNS maison sans getaddrinfo/libc — lit resolv.conf, UDP + repli TCP, politique IPv6-first/IPv4-fallback). Le parsing des réponses DNS est de la donnée hostile → fuzzing obligatoire (cargo-fuzz des décodeurs) à l’implémentation.
  • air-crypto — exceptions 80 % + air-tls séparé (2026-06-12). Zéro crypto maison : RustCrypto (SHA2/3, BLAKE3, Ed25519/X25519, AEAD, HKDF/Argon2), zeroize, subtle (constant-time) sont des exceptions à la règle des 80 % (Principe 6, nommées comme icu4x/ADR-016) → à acter dans DEPENDENCIES.md/docs/EXCEPTIONS.md à l’implémentation. Cœur moderne uniquement (RSA/legacy différés) ; TLS différé à une spec air-tls séparée. Rigueur : vecteurs de test connus (RFC/NIST) + fuzzing des décodeurs.
  • io_uring 3a — différés (dette, PR #40, 2026-06-12). Deux opcodes non livrés au Temps 3a, par décision BDFL : submit_files_update (op 20, mise à jour asynchrone de la table de FD fixes dans le flux) et msg_ring_fd (MSG_SEND_FD) — plomberie d’op async (slab S1 d’un tableau de FD) tangentielle à la registration. La mise à jour synchrone (FixedFdTable::set/clear, register opcode 14) couvre le besoin immédiat. À reprendre avec le runtime async (ou un addendum 3a-bis). (Écarts kernel 3a — headers bornés à l’opcode 26, register_clock(Realtime)→EINVAL [variante conservée], CLOEXEC refusé sur descripteur direct, register_napi→EINVAL hors NIC/NAPI — documentés dans la spec 3a §13 bis et COVERAGE-EXCEPTIONS.md.)
  • io_uring — io-uring-overview.md décrit la sémantique soft/hard À L’ENVERS (dette, PR #44, 2026-06-12). L’ancien overview inverse soft (IO_LINK) et hard (IO_HARDLINK). La référence correcte est la spec 3c §2 (vérifiée contre io_uring_enter(2) et confirmée sur kernel réel 6.17) : soft rompt la chaîne sur erreur de complétion (short read inclus), hard ne rompt pas. À corriger dans io-uring-overview.md lors de sa réconciliation (son rôle d’inventaire est déjà repris par le doc maître ; pas bloquant).

Historique des sessions

Sessions 2026-06-16 → 2026-07-08 — Sceau couche 1, campagnes libc, PAL chantiers A/B (résumé de rattrapage)

Le suivi fin a été tenu dans etat-avancement.md (doc vivant) et la mémoire agent ; cette entrée consolide l’arc pour l’archive. Détail décisionnel dans les ADRs 062→076.

  • Couche 1 SCELLÉE couche-1-v1.0 (2026-07-03, ADR-062) puis re-scellée en additif jusqu’à v1.6 : errno_location (ADR-065), signaux/futex (ADR-066), air-account (ADR-067), credentials + drop_to_user (ADR-068), campagne réseau registre air-handle + moteur socket air-socket (ADR-069).
  • Toit libc C-ABI (scopée OpenSSH) : M0→M4 (crt/errno, str/mem, malloc+printf fuzzé, FILE*, fileio, signaux+pthread, temps, comptes passwd/group/shadow, identifiants) + surface socket complète (30 symboles libair_c.so : inet, cycle de vie, transfert, sockopt, getaddrinfo, poll/select, sendmsg/recvmsg/SCM_RIGHTS). Descellements couche 0 v1.10 (ADR-070 sockaddr + ADR-071 sockopt). CI basculée x86 primaire + cross-check/natif aarch64 (ADR-072).
  • Doctrines : configuration binaire (ADR-073), vision air-sshd (ADR-074 — la vraie cible ; OpenSSH-compile n’était qu’une fonction de forçage couche 1), fourniture du PAL (ADR-075).
  • PAL chantier A COMPLET : 6 briques additives couche 1 (Condvar, Once, os_str, env-mutation, yield/name, destructeurs thread_local). Bug de fondation aarch64 corrigé (gdb : le TCB 64 o recouvrait la zone TLS tpidr_el0+16) — validé 2 arches.
  • PAL chantier B — option A (ADR-076) : std natif sur la libc d’Air (pas la glibc) ; cible env=musl+vendor=air. build-std=std compile entièrement pour Air, zéro patch. Cible de production : compléter libair pour lier stdhello-std on-target 2 arches.

Session 2026-06-15 — Extension additive du sceau couche 0 → couche-0-v1.1 (PR #77)

Réouverture délibérée et additive du sceau couche 0 (décision BDFL), pilotée par les contrats couche 1découverte en Passe 2 d’air-base-lib (073) : sa table From<Errno> mappe ETIMEDOUT/EPIPE/ECONNRESET, que air_sys_types n’exposait pas (29 constantes ; ETIME 62 ≠ ETIMEDOUT 110). Ni amendable sans réouverture (couche 0 scellée), ni « inventable » en numéros bruts en couche 1 (ADR-032) → arrêt-signal (073), puis cette extension. PR #77 mergée (merge commit 7d03932, code 32a1f13 signé GPG + DCO), barrière complète verte 2 arches (la barrière du nouveau sceau). Re-sceau : tag couche-0-v1.1 (signé GPG) sur le merge commit ; couche-0-v1 conservé comme jalon d’origine.

Constantes ajoutées (valeurs asm-generic, identiques x86_64/aarch64), chacune justifiée par un contrat landé, rien de spéculatif :

  • ETIMEDOUT (110), EPIPE (32), ECONNRESET (104) — air-base-lib §1 (From<Errno>TimedOut/BrokenResource) ;
  • ELOOP (40), EXDEV (18) — air-filesystem (# Errors) ;
  • alias EWOULDBLOCK ≡ EAGAIN (valeur 11), pour le mapping par nom.

Display reste basé sur la valeur brute (aucun bras per-const) → zéro branche ajoutée ; couvrable VIDE maintenu (lignes 97,74 % / branches 82,73 %), aucune nouvelle exception ADR-035. Le mapping From<Errno> d’air-base-lib est désormais satisfiable par nom → la Passe 2 (073) est débloquée.

Session 2026-06-14 (suite) — Désactivation du trailer Co-Authored-By mergée (PR #74)

CI-only (prompts 069/070). PR #74 mergée (merge commit 4f48144, code 41c6cb6 signé GPG + DCO), CI verte 2 arches avant merge (couverture sautée : couche0=false, ADR-036). Le trailer Co-Authored-By: Claude est désactivé au niveau du repo via .claude/settings.json versionné ({"attribution":{"commit":"","pr":""},"includeCoAuthoredBy":false}) ; .gitignore tracke uniquement settings.json (.claude/* + !.claude/settings.json ; settings.local.json/claude_session.id restent ignorés). Le Signed-off-by (DCO) est conservé — seul Co-Authored-By est retiré. Décision : pas de réécriture de l’historique ; les commits passés (qui portaient le trailer jusqu’à #65) sont conservés tels quels. Le comportement était un défaut client de Claude Code (auto-attribution suite à une mise à jour), pas une consigne du projet : Claude Code n’est en aucun cas l’auteur ; c’est un outil d’automatisation, comme l’est un compilateur. Vérification définitive de la prise d’effet : les commits du prompt 071 (session fraîche post-merge) ; sinon bug client #18253 → repli ~/.claude/settings.json.

Session 2026-06-14 (suite) — Filtrage par chemin de la re-vérification couche 0 mergé (PR #70, ADR-036)

CI-only (prompts 064/065). PR #70 mergée (merge commit 200b374, code 9321036 signé GPG + DCO), CI verte 2 arches avant merge. ADR-036 landé (filtrage par chemin de la re-vérification d’une couche scellée ; SUMMARY/INDEX/ registre v3.4). La couche 0 scellée n’est plus re-mesurée (couverture lignes/branches) sur les PR qui ne touchent pas couche 0 / toolchain / CI / politique de couverture : job changes (git diff, zéro nouvelle dépendance d’action) → variable FULL ; étapes de couverture gatées if: env.FULL == 'true'. Re-vérification COMPLÈTE toujours garantie : (a) PR touchant ces chemins, (b) merge pushmain (main jamais non vérifié — dérive de toolchain bloquée au merge ; confirmé : le run post-merge sur main a re-mesuré la couverture, vert 2 arches), (c) planifié hebdomadaire (schedule cron lundi 04:00 UTC). Une PR couche 1 pure garde fmt/clippy/cargo test --workspace (couche 0 bâtie + testée), couverture sautée. Mécanique de déterminisme de couverture inchangée (purge .profraw, sudo -n, restauration de propriété de target/) — ne tourne que dans la branche complète. Doc : docs/CI.md (section « Filtrage par chemin… ADR-036 »).

Session 2026-06-14 (suite) — sccache branché sur les runners mergé (PR #68)

CI-only (prompts 062/063). PR #68 mergée (merge commit 4541cbd, code 82ff55d signé GPG + DCO), CI verte 2 arches avant merge. sccache comme RUSTC_WRAPPER sur le job test-coverage (store hors target/, $HOME/.cache/sccache → survit au git clean) : install par binaire pré-bâti musl à SHA256 épinglé (v0.10.0, fallback cargo install signalé). La recompilation de la couche 0 scellée par clippy/test est éliminée100 % de hit à cache chaud sur le Pi (53/53, 0 miss) ; durée test-coverage Pi 6 m 23 s (froid) → 5 m 46 s (chaud). Les passes llvm-cov en root sont volontairement non cachées (choix (b), ADR-031 : sous sudo sccache viserait le cache de root, et -C instrument-coverage se cache mal) → gros gain restant = ADR-036 (filtrage par chemin de la re-mesure de couverture). Garde-fou : le gate cargo xtask repro neutralise RUSTC_WRAPPER (preuve de déterminisme bâtie sans cache, vérifié). Doc : docs/CI.md (section « Cache de compilation — sccache »).

Session 2026-06-14 (suite) — Gates qualité gravés en cargo xtask mergés (PR #66) + bascule doc

Post-sceau (prompts 060/061). PR #66 mergée (merge commit 380d389, code def6c1a signé GPG + DCO), CI verte sur les 2 arches avant merge. Crate xtask (dev-tooling, membre du workspace, zéro dépendance externeCargo.lock du produit et repro ADR-025 intacts ; exclue de la couverture via --exclude xtask, non soumise au 100 %). 5 gates (sortie structurée + --json, exit ≠ 0 sur violation, pour CC et la CI) :

  • barrier : un seul point d’entrée (fmt stable+nightly, clippy -D warnings, test, couverture lignes+branches root, audit/deny/machete, pré-filtre // SAFETY: advisory, cohérence Cargo.toml↔DEPENDENCIES.md) — VERTE.
  • couvrable-vide (anti-gaming) : llvm-cov ⨯ registre. Gate DUR = plancher (lignes 97,73 % / branches 82,73 %) ; réconciliation par symbole/ section/proximité ADVISORY (la reachability n’est pas mécaniquement décidable, cf. CI.md) ; sépare prod / test-resident.
  • check-syscalls : numéros déclarés (// SAFETY: SYS_x (arch = N) + consts) vs headers uapi des 2 arches (x86_64 unistd_64.h, aarch64 asm-generic, indirection __NR3264_* résolue) — 161 conformes, 0 écart (garde la classe d’erreur « privsep ARM64 »).
  • audit-exceptions : format/taxonomie ADR-035 + recompte du récapa corrigé une dérive : CHILD-EXIT 9→8, total 70→69 (sur-comptage introduit au 7b00e75/055, pas une rangée perdue — décision Thierry). Registre désormais à 69 entrées (STRUCTURAL 38, FEATURE-KERNEL 13, PRIVILEGE 10, CHILD-EXIT 8, DEFERRED-TOOLING 0).
  • repro : double-build bit-pour-bit en env contrôlé (SOURCE_DATE_EPOCH, --remap-path-prefix, toolchain pinné) — 4 rlibs identiques → atteste ADR-025.

Parseurs testés sur entrées synthétiques (34 tests). CI : job supply-chain exécute check-syscalls + audit-exceptions ; étapes llvm-cov gagnent --exclude xtask. Doc : docs/outillage-xtask.md (+ SUMMARY, CI.md). Dettes de sceau RÉSORBÉES : reproductibilité ADR-025 outillée (repro), re-sync fichier:ligne outillée (audit-exceptions).

Session 2026-06-14 (suite) — Taxonomie d’exceptions ADR-035 + 3 candidates COUVERTES mergées (PR #60)

Suite de la passe de consolidation (le CODE de la couche 0 reste complet). PR #60 mergée (merge commit aa47d74, code 7b00e75 signé GPG + DCO), CI verte 2 arches.

  • 3 candidates « atteignables-mais-dures » COUVERTES par de vrais tests déterministes (leurs exceptions supprimées) :
    • fs::try_lock contention → Ok(false) : harnais multi-processus forké (clone3) — enfant détient le verrou exclusif, parent try_lockOk(false), synchro par pipes (sans sleep), récolte waitid.
    • net::get_so_error (bras val != 0) : connect non bloquant vers un port loopback réservé-puis-fermé → ECONNREFUSED via SO_ERROR, boucle bornée.
    • process::pidfd_send_signal(Some(&info)) : bras Some exercé via un pidfd de soi-même + SignalInfo::new_queue (déjà public — aucun ajout de surface) ; le kernel rejette par EINVAL (si_signo=0 ≠ sig) avant toute délivrance → sûr même vers soi-même.
    • DEFERRED-TOOLING VIDE / couvrable VIDE au sens strict.
  • Taxonomie appliquée à COVERAGE-EXCEPTIONS.md (ADR-035) : EFAULT-SAFE + VALUE-UNREACHABLE permanente → STRUCTURAL ; CHILD-EXIT conservée ; TEST-HARNESS retiré → annexe « résidus internes aux tests » (hors décompte). Récap recompté depuis les lignes réelles (figures antérieures dérivées) : 70 entrées production (STRUCTURAL 38, FEATURE-KERNEL 13, PRIVILEGE 10, CHILD-EXIT 9, DEFERRED-TOOLING 0). (⚠️ ce recompte a introduit un sur-comptage CHILD-EXIT 9 au lieu de 8 — total réel 69, corrigé au 061 par le gate xtask audit-exceptions, cf. session 2026-06-14 « Gates qualité xtask ».)
  • ADR-035 (« Taxonomie des exceptions de couverture », companion d’ADR-031, sans l’amender) créé ; registre ADRs (table + historique v3.3), SUMMARY.md, INDEX.md.

Dette consignée : la re-vérification durable des fichier:ligne du registre d’exceptions (numéros sujets à dérive) — passer à des renvois par symbole ou un check automatisé (futur xtask audit-exceptions). Prochain chantier → clôture couche 0 : doc d’introduction développeur (introduction-fr.md, encart « dev C/C++ ») + reflet de Q4 (seccomp primitif) dans family-security + réconciliation INDEX/état → traduction anglaisesceau barrière complète.

Session 2026-06-14 — Consolidation couche 0 : test flaky uevent durci mergé (PR #58)

Première étape de la passe de consolidation (le CODE de la couche 0 reste complet). PR #58 mergée (merge commit e74d96e, code 04e6a41 signé GPG + DCO), CI verte 2 arches.

  • Correctif test flaky : device::tests::uevent_read_nonblock_without_event_is_eagain (socket uevent groupe KERNEL NONBLOCK exigeant EAGAIN) cassait par intermittence quand un vrai uevent kernel (hotplug/udev) était diffusé pendant la fenêtre sur un runner partagé. Remplacé par uevent_read_nonblock_returns_promptly_eagain_or_valid_event qui teste le vrai contrat NONBLOCK : retour prompt = soit EAGAIN (file vide), soit un UEventMessage bien formé (événement parasite décodable) ; jamais bloquer/ paniquer/trame malformée (EINTR toléré, ADR-021 c.2). Boucle de robustesse (256 lectures/run) ; prouvé non-flaky (20/20). Changement test-only.
  • Normalisation du registre COVERAGE-EXCEPTIONS.md (non controversée, aucun reclassement) : la catégorie TEST-HARNESS (2 entrées) était utilisée sans être ni définie (légende) ni comptée (récap) → ajoutée (Total 60 → 62) ; note d’audit + date.
  • Audit du registre rendu (053-consolidation-flaky-exceptions.out) : 3 candidates « atteignables-mais-dures » signalées (net::get_so_error, fs::try_lock contention, process::pidfd_send_signal(Some)) ; proposition de taxonomie (fusion EFAULT-SAFE/VALUE-UNREACHABLE-permanent → STRUCTURAL, garder CHILD-EXIT, retirer TEST-HARNESS, addendum ADR) — décisions BDFL prises ; application au prompt 055.

Prochain chantier → 055 : appliquer la taxonomie d’exceptions retenue + couvrir les 3 candidates atteignables + addendum ADR-035. Puis : doc d’intro développeur couche 0, traduction EN, sceau barrière complète.

Session 2026-06-13 (suite) — fs::inotify + affinité CPU mergés (PR #55) — 🎉 CODE couche 0 COMPLET

Passe de complétion couche 0 : deux primitifs indépendants révélés par la spécification de la couche 1, livrés en un PR (modules disjoints d’io_uring). PR #55 mergée (merge commit a009f9b, code b609b87 signé GPG + DCO), CI verte 2 arches.

  • fs::inotify (air-sys-syscall::fs::inotify + types air-sys-types::fs) : RAII Inotify (IN_CLOEXEC par défaut) ; inotify_init/add_watch/ remove_watch/read_events ; décodeur emprunté zéro-allocation des inotify_event de taille variable, zéro-perte (ADR-032) — queue tronquée signalée (InotifyEvents::truncated()), jamais avalée. Syscalls vérifiés uapi 6.12 (init1 294/26, add_watch 254/27, rm_watch 255/28). Tests : intégration kernel réelle (CREATE/MODIFY/MOVED_FROM↔MOVED_TO par cookie/DELETE/IGNORED) + décodeur pur + property-based + fuzz 3 M runs (0 crash). Décodeur 100 %/100 %. fanotify hors périmètre ; récursivité = couche 1.
  • Affinité CPU (famille process, spec family-process-affinity.md créée) : set_cpu_affinity/get_cpu_affinity(Option<Tid>, &CpuSet) ; None = tâche appelante (pid 0 typé) ; CpuSet réutilisé depuis air-sys-types::system. Syscalls vérifiés (setaffinity 203/122, getaffinity 204/123). Tests : round-trip set→get, EINVAL/ESRCH, cross-arch. Débloque air-thread::cpu_affinity (couche 1).
  • +1 exception STRUCTURAL (épuisement ressources inotify_init + casts défensifs i64→i32). Aucune dépendance de production ajoutée. Aucun écart kernel.

🎉 CODE de la couche 0 COMPLET : io_uring (12 Temps) + 11 familles de syscalls

  • fs::inotify + affinité CPU — tous implémentés et mergés pour la cible 6.12 (hors interfaces obsolètes → UNSUPPORTED.md). Plus de code à écrire. Prochain chantier → passe de consolidation couche 0 : audit du registre d’exceptions, doc d’introduction développeur, traduction anglaise globale, réconciliation INDEX/état, sceau barrière complète. Différés conservés (epoll types spéculatifs, futex synchrone v2, fanotify, différés io_uring 3a).

Session 2026-06-13 (suite) — io_uring Temps 4 (accès brut) mergé (PR #53) — 🎉 MODULE io_uring COMPLET

Sous-module air-sys-syscall::io_uring::raw : la soupape de sécurité d’ADR-022 (Décision 1) — manipulation directe des SQE/CQE pour les ~5 % de cas que l’API typée (Temps 1–3f) ne couvre pas. L’ordering d’anneau reste géré par la façade ; l’unsafe ne porte que sur le contenu du SQE et la validité des buffers. PR #53 mergée (merge commit c91c693, code 9fe14fb signé GPG + DCO), CI verte 2 arches.

  • Types miroirs #[repr(C)] : RawSubmissionQueueEntry (#[repr(transparent)] sur IoUringSqe, 64 o), RawCompletionQueueEntry (16 o), RawOpcode(u8) ; assertions statiques de taille/offset (uapi 6.12, identiques x86_64/aarch64) → dérive = échec de compilation. Accesseurs typés (pas de calcul d’offset manuel).
  • Soumission brute : raw_get_submission_queue_entry (unsafe) ; publication = submit()/submit_and_wait() (sûrs : store release par la façade). Complétion brute : raw_peek_completion_queue_entry (sûr, load acquire, réf. bornée) + raw_advance_completion_queue (sûr, borné). Capacités SQ/CQ. Aucun pointeur mmap nu exposé.
  • Coexistence brut/niveau 2 (§5) : RAW_USER_DATA_TAG = 1 << 63. La moisson gérée court-circuite sur le tag (slab non touché, CQE non consommé) ; le slab borne sa génération à 31 bits (GENERATION_MASK) ⇒ le bit 63 du user_data géré est toujours nul → brut et niveau 2 coexistent sur le même ring sans collision. debug_assert du tag manquant (testé via #[should_panic]).
  • Tests : layout statique ; intégration kernel (NOP brut ↔ raw_peek/raw_advance ; op brute vs wrapper niveau 2 ; coexistence slab+brut sans collision ; SQ pleine → None ; advance borné ; moisson gérée qui s’efface devant une brute en tête) ; Miri (chemins purs : layout, slab génération 31 bits — 16 tests verts) ; fuzz du décodage RawCompletionQueueEntry (cible io_uring_4_raw_cqe, 2 M runs, 0 crash). raw.rs 100 % lignes / couvrable vide (aucune exception propre au Temps 4) ; ring.rs 100 %/100 %.
  • Correctif annexe (ADR-032) : 3 littéraux Completion de fuzz_api (#[cfg(fuzzing)], non bâti en CI) restés sans le champ multishot depuis le Temps 3d → la fuzz-cible ne compilait plus ; dette silencieuse purgée. Aucun écart kernel.

🎉 Module io_uring COMPLET : les 12 Temps (1, 2a–2d, 3a–3f, 4) sont implémentés et mergés pour la cible 6.12 (hors interfaces obsolètes → UNSUPPORTED.md). Prochain chantier → clôture couche 0 : fs::inotify + sched_setaffinity, puis passe de consolidation. Dette : traduction anglaise globale des documents du module io_uring (à planifier).

Session 2026-06-13 (suite) — io_uring Temps 3f (confinement) mergé (PR #51)

Sous-module air-sys-syscall::io_uring::sandbox : confine un ring pour qu’il ne puisse émettre que les opérations explicitement mises en liste blanche, imposé par le kernel — matérialise la soundness S3. Brique io_uring du modèle de capabilities d’Air (ADR-001 AirCom, ADR-010 entitlements signés), en défense en profondeur avec family-security (seccomp/Landlock). PR #51 mergée (merge commit 39aa92f, code eb494c0 signé GPG + DCO), CI verte 2 arches (coverage root).

  • RestrictionSet (default-deny, #[derive(Default)]) : allow_op / allow_register / allow_sqe_flags / require_sqe_flags + from_entitlements (point d’intégration couche 5, ADR-010 — la politique vit en couche 5). RegisterOp (énum typée des register-ops, ADR-021 conv. 3 — pas de u8 magique) ; SqeFlagSet (drapeaux IOSQE_*, partage la repr. de SubmitOptions).
  • Flux en 3 temps imposé kernel + reflété par l’API : IoUringBuilder::restrict → ring R_DISABLED + REGISTER_RESTRICTIONS ; IoUring::enableREGISTER_ENABLE_RINGS ; restrictions immuables ensuite. Default-deny : dès un allow_op, le kernel refuse tout le reste (-EACCES). Le stub ENOSYS de build() (Temps 1) est remplacé par le vrai REGISTER_RESTRICTIONS (test obsolète restrict_then_build_is_refused_with_enosys retiré).
  • Preuve de sécurité (§8) : openat2 REFUSÉ (-EACCES) sur un ring « réseau seul » alors que le process a le droit d’ouvrir le fichier → la voie de contournement de seccomp est fermée. Aussi : opcode autorisé qui passe, default-deny, require_sqe_flags imposé, soumission avant enable refusée (EBADFD), immuabilité après enable (refus kernel), propagation d’erreur REGISTER_RESTRICTIONS (simulateur), property-based (tout sous-ensemble encode exactement ses opcodes).
  • Couverture (root) : sandbox.rs 100 % lignes + branches / couvrable vide ; unique résidu = conversion défensive usize→u32 du nombre de restrictions (STRUCTURAL, inatteignable). raw : REGISTER_RESTRICTIONS (11), io_uring_register_restriction_op (0/1/2/3), miroir io_uring_restriction (16 o, assert). TOTAL lignes 96,2 % / branches lib 78,2 %. Aucun écart kernel. Prochain chantier → io_uring Temps 4 (raw), dernier du module.

Session 2026-06-13 (suite) — io_uring Temps 3e (multi-thread) mergé (PR #49)

Sous-module air-sys-syscall::io_uring::shared : les modèles d’usage multi-thread du ring, par-dessus le cœur Temps 1, ATTACH_WQ (3a) et msg_ring (2c). Aucun register opcode propre. PR #49 mergée (merge commit f9be984, code 43c957a signé GPG + DCO), CI verte 2 arches (coverage root).

  • unsafe impl Send for IoUring réalisé (ADR-022 D6, documenté depuis le Temps 1 mais jamais implémenté : les pointeurs bruts mmappés rendaient IoUring auto-!Send). // SAFETY: : mmaps/FD process-global, Send = transfert d’ownership exclusif, !Sync préservé (PhantomData<Cell>). Invariant habilitant de tout le Temps. Surface publique inchangée (impl auto).
  • LockedIoUring (Mutex<IoUring>, Send + Sync) : primitive with_lock (couvre toute op d’IoUring en &self) + raccourcis nommés fréquents — pas de ~50 forwarders (Principe 7) ; verrou unique, sans poisoning.
  • RingPool (thread-per-core) : le 1er ring crée le pool io-wq, les suivants ATTACH_WQ (workers kernel bornés, Pi 4) ; ring fds enregistrés ; into_rings (un ring possédé par worker, déplacé) ; handle(w) -> RingHandle
    • submit_message_ring_data_to (routage msg_ring cross-thread par fd ; le 2c, qui prend &IoUring, reste inchangé). SqpollIoUring : SETUP_SQPOLL
    • SQ_AFF, soumission sans io_uring_enter en régime établi, réveil NEED_WAKEUP géré (seule magie cachée, documentée).
  • Soundness (cœur du Temps) : IoUring: !Sync prouvé par doctest compile_fail, LockedIoUring: Send+Sync par doctest + assertion statique ; loom sur la discipline de verrou (exclusion mutuelle, aucune mise à jour perdue) ; tests d’intégration concurrents réels (threads).
  • Couverture (root, après complément 046/PR #49) : shared.rs branches 100 % / couvrable vide — l’exception EFAULT-SAFE sur les bras enter SQPOLL est levée (couverts au simulateur : injection -EINTR sur la couture syscall::enter, errno remonté tel quel ADR-021) ; résidus = fallback SQPOLL-EPERM des tests (PRIVILEGE) + timeout du thread pair (TEST-HARNESS). Flakiness réelle corrigée : locked_io_uring_concurrent_nops (course de complétion) rendu atomique sous un seul with_lock. TOTAL lignes 96,1 % / branches lib 78,1 %. Aucun écart kernel. Prochain chantier → io_uring Temps 3f (confinement).

Session 2026-06-13 — io_uring Temps 3d (opérations multishot) mergé (PR #46)

Sous-module air-sys-syscall::io_uring::multishot : une unique soumission produit un flux de complétions (CQE_F_MORE), par-dessus le cœur Temps 1, les buffers fournis du 3b et les descripteurs directs du 3a. Aucun register opcode ; opcode dédié READ_MULTISHOT (49) + drapeaux d’op. PR #46 mergée (merge commit ba26ff9, code 00ccee3 signé GPG + DCO), CI verte 2 arches (coverage root, raspi a bâti 00ccee3).

  • Cycle de vie S1 : slot vivant tant que CQE_F_MORE, libéré uniquement à la terminale (2ᵉ cas après le NOTIF zero-copy du 2b). Slab étendu (reserve_multishot/is_multishot + champ multishot du slot) ; Completion porte un marqueur multishot lu avant complete ; MultishotToken (sémantique fixée) ; Completion::multishot_token() ; génération anti-CQE-tardif prouvée Miri (test pur du slab).
  • Façades : submit_accept_multishot[_direct] (ACCEPT + ACCEPT_MULTISHOT ; variante directe = slot auto-alloué FixedFdTable), submit_receive_multishot (RECV + RECV_MULTISHOT) / submit_read_multishot (READ_MULTISHOT 49) sur buffers fournis, submit_poll_multishot (POLL_ADD + POLL_ADD_MULTI, edge), submit_timeout_multishot (TIMEOUT + TIMEOUT_MULTISHOT, ticks), cancel_multishot. Pénurie -ENOBUFS TERMINE le multishot, distincte d’une erreur réseau (testé).
  • Choix signalés (surface inchangée) : cancel_multishot utilise l’annulation synchrone (IORING_REGISTER_SYNC_CANCEL par jeton) plutôt que l’op ASYNC_CANCEL (cohérent avec -> Result<()>, pas de CQE d’annulation à drainer) ; submit_poll_multishot edge-triggered (la surface ne porte pas d’option level-triggered ; IORING_POLL_ADD_LEVEL = 1<<3 non exposé).
  • Couverture (root) : multishot.rs 100 % lignes + branches / couvrable vide ; cycle S1 prouvé sous Miri. TOTAL lignes 97,7 % / branches 81,9 %. 9 tests d’intégration + 2 tests slab purs. Aucun écart kernel. Prochain chantier → io_uring Temps 3e (multi-thread).

Session 2026-06-12 (suite) — io_uring Temps 3c (opérations liées) mergé (PR #44)

Sous-module air-sys-syscall::io_uring::linked : chaînes d’opérations liées (IOSQE_IO_LINK/IO_HARDLINK + LINK_TIMEOUT op 15), par-dessus le cœur Temps 1. Aucun register opcode. PR #44 mergée (merge commit 383eb99, code 575d3ab signé GPG + DCO), CI verte 2 arches (coverage root, raspi a bâti 575d3ab).

  • LinkedChainBuilder via ring.link_chain() : first / then (soft, IO_LINK sur le prédécesseur) / then_hard (IO_HARDLINK) / with_link_timeout (LINK_TIMEOUT bornant le maillon précédent ; émission isolée refusée) / submit() -> ChainTokens.
  • Mode staging : les submit_* existants réservent slot + écrivent le SQE (drapeau de lien sur le prédécesseur) sans publier la queue ; publication unique au submit() (un seul store-release). Aucune duplication des prepare_*. Réservation atomique : un maillon non réservable (EBUSY) déclenche un rollback (release idempotent + rewind de la tail) — jamais de chaîne partielle publiée.
  • Sémantique soft/hard CORRECTE confirmée sur kernel réel (6.17) : soft rompt sur erreur de complétion — un short read COMPTE comme erreur (piège testé explicitement) → suivants -ECANCELED ; hard ne rompt pas. link_timeout expirant (maillon annulé) et non-expirant (timeout annulé) testés.
  • skip_cqe_on_success sur les intermédiaires (seule la finale arrive) ; ChainTokens corrèle chaque complétion à son maillon. Helpers ring.rs (local_tail/or_sqe_flags/rewind_to) ; raw.rs (IOSQE_IO_LINK/HARDLINK, OP_LINK_TIMEOUT).
  • Couverture (root) : linked.rs lignes de production 100 % / couvrable vide (résidu branches = court-circuits d’assertions de test ; garde STRUCTURAL préexistante de prepare). TOTAL lignes 97,7 % / branches 81,7 %. 14 tests + ChainTokens sous Miri. Aucun écart kernel. Prochain chantier → io_uring Temps 3d (multishot).

Session 2026-06-12 (suite) — io_uring Temps 3b (buffers fournis ring-mapped) mergé (PR #42)

Sous-module air-sys-syscall::io_uring::provided : buffers fournis ring-mapped (IORING_REGISTER_PBUF_RING 22 / UNREGISTER 23 / PBUF_STATUS 26), par-dessus le cœur Temps 1 et la registration Temps 3a. PR #42 mergée (merge commit 8ca051c, code e56eba0 signé GPG + DCO), CI verte 2 arches (coverage root, raspi a bâti e56eba0).

  • ProvidedBufferRing : register (count puissance de 2, buf_size ; modes app et kernel_mmap), unregister, status, group_id, available. Anneau de descripteurs io_uring_buf[count] (tail u16 @ offset 14, avancé par store-release) + mémoire de données possédée (count×buf_size).
  • Sélection auto : submit_receive_provided/submit_read_provided (IOSQE_BUFFER_SELECT, aucun buffer au slot S1) + bundle: bool (IORING_RECVSEND_BUNDLE). Pénurie ⇒ complétion -ENOBUFS.
  • Guard RAII ProvidedBuffer via Completion::into_provided_buffer(&mut group)
    id/data/data_mut ; Drop réapprovisionne (réécrit le descripteur, store-release du tail). L’emprunt &mut interdit le désenregistrement tant qu’un buffer vit (doctest compile_fail).
  • Incrémental (IOU_PBUF_RING_INC) : CQE_F_BUF_MORE ⇒ même buffer réutilisé, pas de réappro ; réappro à la complétion finale. Accounting available idempotent (out_flags par bid).
  • Legacy évacuésdocs/UNSUPPORTED.md créé : PROVIDE_BUFFERS (31), REMOVE_BUFFERS (32), + OPENAT (18).
  • Écarts kernel signalés (spec 3b §9 bis, COVERAGE-EXCEPTIONS.md) : IOU_PBUF_RING_INC/RECVSEND_BUNDLE/CQE_F_BUF_MORE absents des headers exécuteurs (uapi 6.11/6.12, validés au runtime 6.17) ; bloc-code spec §3 aligné sur la prose (param bundle ajouté à la signature, surface effective inchangée).
  • Couverture (root) : provided.rs lignes 99,6 % / couvrable vide (résidu = bras ? des mmap = EFAULT-SAFE ; gardes bid<count = STRUCTURAL). TOTAL lignes 97,7 % / branches 82,0 %. 9 tests (intégration : recv loopback, -ENOBUFS+réappro, kernel_mmap, status, read/bundle ; synthétiques déterministes BUF_MORE/réappro/None ; property 256 cycles ; simulateur) + 1 doctest compile_fail. Prochain chantier → io_uring Temps 3c (opérations liées).

Session 2026-06-12 (suite) — io_uring Temps 3a (registration) mergé (PR #40)

Sous-module air-sys-syscall::io_uring::registration : les 21 register opcodes (io_uring_register(2), n° 427) du Temps 3a, livrés par-dessus le cœur Temps 1. PR #40 mergée (merge commit 34c164e, code aefec1a signé GPG + DCO), CI verte 2 arches (x86_64 speedy + aarch64 raspi-srv-2, coverage root).

  • FixedFdTable (FILES2/FILES_UPDATE2/FILE_ALLOC_RANGE/UNREGISTER_FILES) : register(sparse)/register_with/set/clear/set_alloc_range/unregister (restitue tous les OwnedFd — zéro discard) / slot() -> FixedSlot<'t>.
  • Variantes « direct » : submit_openat2_direct/accept_direct/socket_direct
    • FixedSlotTarget::{Index, Alloc} (Alloc = enum typé, jamais ~0U exposé). Découverte kernel : SOCK_CLOEXEC/O_CLOEXEC rejeté (EINVAL) sur un descripteur direct ⇒ non posé (le O_CLOEXEC est posé à fixed_fd_install). Usage : submit_read_fixed_file/write_fixed_file (via FixedSlot), submit_fixed_fd_install (FD direct → FD ordinaire).
  • RegisteredBuffers (BUFFERS2/BUFFERS_UPDATE/CLONE_BUFFERS/UNREGISTER_BUFFERS)
    register(Vec)/register_mmap(MmapRegion possédées)/update/clone_from/ unregister/slice() -> RegisteredBufferSlice<'b> + submit_read_fixed/write_fixed.
  • Ring fd enregistré (RING_FDS/UNREGISTER_RING_FDS) : câblé dans le chemin io_uring_enter (IORING_ENTER_REGISTERED_RING). eventfd (4/5/7), personality (9/10, + IoUring::with_personality), io-wq (IOWQ_AFF 17/18, MAX_WORKERS 19 → WorkQueueWorkerLimits), NAPI (27/28), horloge (29).
  • Types ajoutés : FixedFdTable/FixedSlot/FixedSlotTarget/RegisteredBuffers/ RegisteredBufferSlice/Personality/WorkQueueWorkerLimits/NapiConfig/ClockSource (sous-module registration) ; CpuSet créé dans air-sys-types::system (miroir cpu_set_t, 1024 bits ; n’existait pas, ADR-029).
  • Sûreté : FixedSlot/RegisteredBufferSlice liés par lifetime → ne survivent pas au désenregistrement (prouvé par doctests compile_fail — pas de dépendance trybuild ajoutée). Téardown S2 inchangé (kernel auto-nettoie au close ; les OwnedFd possédés se ferment au drop, aucune fuite).
  • Écarts kernel signalés (jamais corrigés en silence, ADR-032) : (1) headers installés bornés à l’opcode 26 (IORING_REGISTER_LAST == 27) — NAPI/CLOCK/ CLONE_BUFFERS (27-30) repris de l’uapi 6.12 amont, validés à l’exécution ; (2) register_clock(Realtime) rend EINVAL (io_uring n’accepte que MONOTONIC/ BOOTTIME) — type conservé, rejet testé ; (3) register_napi rend EINVAL sur les exécuteurs (NAPI non exposé) → chemin succès en exception FEATURE-KERNEL.
  • Couverture (root) : registration.rs branches 100 %, lignes 99.2 % (résidu = corps succès NAPI, FEATURE-KERNEL) ; air-sys-types::system (CpuSet) 100 % branches. TOTAL lignes 97.6 % / branches 81.9 % (planchers 96/78). Tests : 23 intégration/simulateur/bornes + 2 doctests compile_fail.
  • Différés (signalés) : submit_files_update (op 20, mise à jour async de la table dans le flux) et msg_ring_fd (MSG_SEND_FD) — plomberie d’op async tangentielle à la registration, proposés en suite ; la table se met à jour synchroniquement via set/clear (register opcode 14). Prochain chantier → io_uring Temps 3b (provided, buffers fournis ring-mapped PBUF_RING).

Session 2026-06-12 (suite) — io_uring Temps 2d (URING_CMD, opcode 46) mergé (PR #37)

Le passthrough io_uring (IORING_OP_URING_CMD, 46) est livré : transmettre une commande opaque au gestionnaire uring_cmd du fichier visé (socket, NVMe, ublk…). Sous-module air-sys-syscall::io_uring::cmd, surface validée (io-uring-2d-cmd.md) non modifiée.

  • 4 commandes socket typées sûres : submit_socket_inq/outq (SIOCINQ/SIOCOUTQinto_result), submit_getsockopt (buffer de sortie ownership S1 → into_buffer_result), submit_setsockopt (value déplacée → completed). level: SocketOptionLevel.
  • Générique : unsafe trait UringCommand (cmd_op/cmd_len/encode/Output/ interpret) + submit_uring_cmd<C> (vérifie cmd_len ≤ 16, ou ≤ 80 si SQE128, EINVAL en amont — pas de troncature) + submit_uring_cmd_raw (unsafe, dernier recours). Structures NVMe/ublk hors couche 0 (mécanisme seul).
  • Types : UringCmdFlags (FIXED = bit 0, seul flag 6.12) ; SocketOptionLevel (air-sys-types::net — n’existait pas, créé, ADR-029 ; la spec le disait « partagé family-net » mais family-net n’utilisait que des i32 bruts).
  • Bug latent corrigé — SQE128 désormais réellement soumissible : la soumission en SETUP_SQE128 était buggée depuis le Temps 1 — SubmissionRing::prepare indexait les SQE par sizeof(IoUringSqe) = 64 o, donc les SQE de 128 o étaient mal placés (stride faux) et partiellement non zéro-initialisés ; seul le setup SQE128 marchait, jamais la soumission (aucun test ne soumettait en 128 o). Corrigé : sqe_size mémorisé, stride en octets, slot entier zéro-init, provenance mmap pour écrire la zone cmd[] étendue ; submit_op factorisé en submit_filled (pointeur brut). Validé sur kernel réel (round-trip 128 o). Dette « SQE128 soumission » RÉSOLUE (PR #37).
  • Ergonomie signalée : UringCommand::interpret(&self) impose au consommateur de conserver une instance de la commande (passée par valeur à submit) pour interpréter la complétion — fidèle à la surface validée.
  • PR #37 (merge commit b7be2ae), commit 8b91ae7 signé GPG + DCO. Barrière verte 2 arches : fmt (stable et nightly — incident rustfmt résolu), clippy -D, tests (591 lib), llvm-cov root (lignes 97.4 %, branches 81.6 % ; cmd.rs 100 % branches, 0 ligne manquante, couvrable vide), Miri (encode borné), fuzz io_uring_2d_decode (200 k runs), audit/deny/machete. Prochain chantier → io_uring Temps 3a (registration).

Session 2026-06-12 (suite) — extension couche 0 process privsep livrée (PR #34)

Le 4ᵉ manque couche 0 (le plus prioritaire, sécurité) est comblé : la famille process reçoit les 6 wrappers indispensables à une réduction de privilèges correcte (drop_privileges d’air-process, Principe 10).

  • 6 wrappers : set_groups(&[Gid]) / get_groups(&mut [Gid]) -> &[Gid] (setgroups/getgroups, &[] = largage total, zéro alloc), set_resgid / set_resuid (Option<Gid/Uid> ×3 ; None = composante inchangée = (id_t)-1 typé, ADR-021 conv. 1) qui fixent le saved-set ⇒ réduction irréversible, et get_resgid -> ResGid / get_resuid -> ResUid (vérif défensive). // SAFETY: sur les 6 wrappers + 12 helpers asm.
  • Types ajoutés (air-sys-types::process) : Uid/Gid (#[repr(transparent)] sur u32 ; contrairement à Pid/Tid, 0 = root est valide ⇒ pas de NonZeroI32 ; ROOT/from_raw/as_raw, types distincts) + ResUid/ResGid. Constat : ils n’existaient pas (ni setuid/setgid).
  • Numéros de syscalls vérifiés sur l’uapi 6.12 par arche — la spec donnait setresgid ARM64 = 143 (FAUX), corrigé en 149 (header asm-generic). Retenus : setgroups 116/159, getgroups 115/158, setresgid 119/149, setresuid 117/147, getresgid 120/150, getresuid 118/148. Spec corrigée embarquée dans #34.
  • Test de sécurité (le cœur), vert en root : privsep_drop_is_irreversible (enfant forké, observé par waitid) — après set_resgid(nobody³) + set_resuid(nobody³), le regain root → EPERM, regain gid → EPERM, réajout de groupe → EPERM ; getres* confirment le saved-set figé. Corps d’enfants sans branche locale (combinés par &) → le flush LLVM impossible après largage n’ajoute aucune branche non couverte.
  • PR #34 (merge commit 7e07525), 3 commits signés GPG + DCO (9bd2335 code, b3b8cbf spec corrigée). Barrière verte 2 arches : fmt, clippy -D, tests (791), llvm-cov root (lignes 97.3 %, branches 81.7 % ; production rouge = 5 bras erreur EPERM/EFAULT ⊆ exceptions PRIVILEGE/STRUCTURAL, couvrable vide), audit/deny/machete. Prochain chantier → io_uring Temps 2d (URING_CMD).

Session 2026-06-12 — MmapRegion livré (PR #31) : madvise (2a) + futex (2c) débloqués/branchés

Le dernier prérequis transverse de la couche 0 io_uring est levé. MmapRegion (mapping partageable refcounté + garde de vivacité) est implémenté dans air-sys-syscall::mem, et submit_madvise (Temps 2a) + submit_futex_* (Temps 2c) sont branchés — les deux reliquats différés sont fermés d’un coup.

  • MmapRegion = Arc<MmapRegionInner> (ptr/len/prot), munmap au dernier drop (erreur ignorée comme Mapping). MmapRegionLiveness (clone de l’Arc) garée dans le slot S1 → munmap impossible tant qu’une op est en vol : ni use-after-unmap, ni fuite. Mapping reste strictement inchangé (RAII unique, zéro-coût, couverture 100 % intacte). Une alloc Arc assumée (exception ADR-021 c.4, opt-in).
  • Soundness prouvée : Miri (ownership : no UAF / no leak / drop-once) + loom (drops concurrents des gardes → munmap exactement une fois, jamais en avance). Intégration kernel réel : madvise WILLNEED/DONTNEED, futex wait réveillé par wake, futex_waitv multi-attente, drop de la région en vol.
  • Ops branchées : submit_madvise(&MmapRegion, range, advice) (OP 25) ; submit_futex_wait/wake(&MmapRegion, offset, …) (OP 51/52), submit_futex_waitv (OP 53). Signature futex corrigée (&AtomicU32 insoundable → &MmapRegion+offset). Types ajoutés : FutexFlags, FutexWaiter ; opcodes + futex_waitv ABI.
  • Trois durcissements d’API appliqués avant merge (PR #31, second commit) : from_mapping(mapping, prot) (prot explicite, plus de défaut READ|WRITE faux → fin d’un SIGSEGV latent sur mapping read-only) ; futex_word exige WRITE (la réf rendue est mutable) ; FutexWaiter sans mask (le struct futex_waitv kernel n’en porte pas — champ jamais transmis, footgun ADR-032). Test ajouté : from_mapping read-only → bytes() OK, futex_word() EINVAL avant accès.
  • Specs réconciliées (FR + EN) : family-mem-mmap-region.md (§1 from_mapping(prot), futex_word/WRITE ; §3 FutexWaiter sans mask), io-uring-2a §6.2 (madvise) et io-uring-2c §6.1 (futex) passent de « différé » à « implémenté ».
  • PR #31 (merge commit b61250e), 2 commits signés GPG + DCO (c9e3fbc impl, b12be19 durcissements). Barrière verte 2 arches : fmt, clippy -D, tests (780), Miri, loom, llvm-cov root (TOTAL lignes 97.7 % / branches 81.9 % ; couvrable du nouveau code = vide), audit/deny/machete, mdbook. Prochain chantier → io_uring Temps 2d (URING_CMD).

Session 2026-06-11 (suite) — io_uring Temps 2c (async) mergé (PR #28)

Implémentation, durcissement et merge du cœur async-spécifique du Temps 2c d’io_uring.

  • 9 façades + 2 accesseurs typés (PR #28, merge commit 1f2e5cb) : submit_nop_with_result, submit_timeout (+ _remove/_update), submit_cancel (async, CancelTarget Token/Fd/Op/Any + CancelFlags::ALL), submit_poll_add (+ _remove/_update), submit_epoll_ctl, submit_waitid, submit_message_ring_data (MSG_DATA) ; accesseurs into_poll_result, into_waitid_result. SignalInfo/WaitTarget/WaitOptions réutilisés de family-process/signal ; KernelTimespec/CancelTarget du Temps 1.
  • Couverture 100 % hors exceptions (root) : lignes 97.4 % / branches 81.7 %. Ensemble « couvrable » VIDE — zéro région ni branche de prod 2c rouge, et AUCUNE exception 2c (pas de garde u32 ici). Miri (ownership des payloads Timeout/Waitid/Epoll restitués sans UAF), proptest (cancel idempotent), fuzz (io_uring_2c_decode).
  • Types ajoutés à air-sys-types : io_uring::{PollEvents, TimeoutFlags, TimeoutSpec, CancelFlags, MessageRingFlags, EpollOp, EpollEvent} ; signal::SignalInfo::zeroed(). L’epfd des tests vient d’un epoll_create1 brut test-only (pas de fausse famille epoll publique).
  • Spec 2c amendée embarquée dans la PR (réconcilie ETIME_SUCCESS n’altère pas le res, cancel sans ALL rend 0, types epoll, futex→MmapRegion).
  • Différés (signalés) : futex (51/52/53) → PR coordonnée family-mem (handle de vivacité MmapRegion, le même que madvise — un seul handle débloque les deux) ; link_timeout (15) → Temps 3c ; files_update (20), fixed_fd_install (54), msg_ring_fd (40 MSG_SEND_FD) → Temps 3a.
  • 2 arches : speedy x86_64 + raspi-srv-2 aarch64 (incl. l’asm epoll_create1 aarch64). Reste couche 0 : io_uring Temps 2d→4, 3a–3f. Prochain : Temps 2d (URING_CMD).

Session 2026-06-11 (suite) — io_uring Temps 2b (réseau) mergé (PR #26) + ADR-032

Implémentation, durcissement et merge du Temps 2b d’io_uring (12 opérations réseau) — stratégique AirCom (ADR-001 : zero-copy + passage de FD).

  • 12 façades réseau + accesseurs typés (PR #26, merge commit 6055b11) : accept/_with_peer, connect, socket, bind, listen, send, receive, send_message, receive_message, send_zero_copy, send_message_zero_copy, shutdown.
  • Cycle zero-copy à DEUX complétions (CQE résultat F_MORE → CQE NOTIF qui restitue) porté par le has_more du slab (Temps 1, sans extension) ; prouvé sûr Miri (le buffer survit à la 1ʳᵉ complétion). Passage de FD SCM_RIGHTS
    FD reçus matérialisés en OwnedFd CLOEXEC, MSG_CTRUNC signalé, aucune fuite. Invariants Air : MSG_NOSIGNAL/SOCK_CLOEXEC par défaut.
  • Types possédés OwnedSendMessage/OwnedReceiveMessage ajoutés à air-sys-types::net : les SendMessageRequest<'a>/ReceiveMessageRequest<'a> de family-net sont basés sur des emprunts (sync), inadaptés à l’ownership asynchrone S1. Sérialisation sockaddr + cmsg réutilisées de net.rs (pub(crate), DRY). ReceiveMessageMeta, ZeroCopyFlags ajoutés.
  • into_zero_copy_buffers (restitution intégrale des buffers d’un sendmsg_zc, jamais le premier seul) — corrige le point relevé au rapport 019. Spec 2b corrigée en conséquence (types possédés + restitution intégrale).
  • ADR-032 (« Préservation des données confiées : zéro discard silencieux ») gravé : règle transverse — une API Air ne discarde jamais une donnée confiée (restitution intégrale, ressources matérialisées, troncatures signalées, chemins d’erreur compris). Registre v3.0.
  • Couverture 100 % hors exceptions (root) : lignes 97.4 % / branches 81.6 %. Ensemble « couvrable » VIDE (résidu = 3 gardes u32 STRUCTURAL, §2b). proptest + Miri + cargo-fuzz (io_uring_2b_recvmsg). 2 arches (speedy + raspi-srv-2). Reste couche 0 : io_uring Temps 2c→4, 3a–3f. Prochain : Temps 2c (async).

Session 2026-06-11 (suite) — io_uring Temps 2a (filesystem) implémenté et mergé (PR #24)

Implémentation, durcissement et merge du Temps 2a d’io_uring (opérations filesystem), par-dessus le cœur du Temps 1.

  • 23 façades FS + 4 accesseurs typés (PR #24, base main @ 26a8bcd, merge commit a2eea18). read/write/readv/writev, fsync/sync_file_range/ fallocate/ftruncate, openat2/close, statx, fadvise, splice/tee, mkdirat/unlinkat/renameat/linkat/symlinkat, 4× xattr ; accesseurs into_vectored_result/into_statx/into_xattr_result/opened_fd. Sans nouveau type de ring : opcodes IORING_OP_* (uapi 6.12) encodés en SQE, état possédé garé dans le slot S1 (slab généralisé à un payload hétérogène OwnedOp), restitution typée à la complétion.
  • Ownership prouvé sound (Miri) : Vec<u8>/Vec<Vec<u8>>+iovec/Box<Statx>/ CString déplacés dans le slot ; un buffer en vol ne peut être ni libéré ni réutilisé. Bug corrigé : submit_close parquait l’OwnedFddouble close (attrapé par le contrôle IO-safety) ; correctif into_raw_fd (cession au kernel, aucun payload garé).
  • Types ajoutés à air-sys-types::fs (conformes family-fs) : FsyncFlags, SyncFileRangeFlags, UnlinkFlags, LinkFlags, XattrFlags, FadviseAdvice (distinct de mem::MadviseAdvice — espaces de valeurs POSIX_FADV_*MADV_*), et un miroir brut Statx #[repr(C)] (256 o, assert de layout).
  • Couverture 100 % hors exceptions (mesurée en root, ADR-031) : lignes 97.1 % / branches 82.1 % au TOTAL (planchers 96/78). Diagnostic 017 : 0 branche de prod 2a couvrable, seules rouges = 6 gardes u32 (buffers > 4 GiB) documentées STRUCTURAL. Tests : intégration kernel 6.12 (round-trips) + proptest (transfert borné + restitution fidèle, chemins non-UTF-8) + simulateur (EBUSY) + Miri + cargo-fuzz (io_uring_2a_decode).
  • Barrière verte 2 arches : x86_64 (speedy natif) + aarch64 (CI self-hosted raspi + run natif fs_ops au SHA bâti). gh pr checks 24 vert sur les 2.
  • Différé (signalé) : madvise (PR coordonnée family-mem, cf. Dettes), read_fixed/write_fixed + variantes direct/fixed (Temps 3a). Reste couche 0 : io_uring Temps 2b→4, 3a–3f. Prochain : Temps 2b (réseau).

Session 2026-06-11 (suite) — device + ebpf implémentées et mergées : 11 familles couche 0 livrées

Implémentation, durcissement (proptest + fuzz) et merge des deux dernières familles de la couche 0, puis bascule ✅.

  • device (PR #20, base main) : uevent netlink (authenticité nl_pid==0/SCM_CREDENTIALS) + evdev (18 ioctls EVIOC*, lecture typée input_event), sysfs renvoyé à fs. InputEvent.sec/usec en i64 (fidélité struct timeval LP64). 100 % hors exceptions + test d’intégration uinput réel + proptest + fuzz fuzz_uevent_parse.
  • ebpf (PR #21, empilée puis rebasée sur main) : bpf() 37 sous-commandes + perf_event_open + 12 ioctls (dont le pont perf_event_set_bpf_program), RAII. Vrai 100 % hors exceptions STRUCTURAL via un harnais d’intégration privilégié réel (programme compteur assemblé main avec BPF_PSEUDO_MAP_FD + raw_tracepoint, cgroup v2 attach/link, bpffs pin/get, btf_load + carte BTF-typée, introspection, perf complet). proptest + fuzz fuzz_bpf_obj_info. Bugs trouvés/corrigés par le harnais (bpf_program_query offsets, bpf_program_test_run pointeurs NULL).
  • CI — mesure de couverture en root (PR #22, ADR-031). ebpf étant quasi-tout-privilégiée, sa couverture plafonnait ~74 % en non-root et faisait tomber le total sous le plancher 96 %. Le job test-coverage exécute désormais les étapes llvm-cov via sudo -n sur les runners self-hosted (NOPASSWD sur les deux) pour que les tests d’intégration privilégiés tournent sous instrumentation. Modèle de confiance dépôt privé/mono-mainteneur, à revisiter avant l’ouverture publique.
  • 11 familles couche 0 livrées (100 % hors exceptions, 2 arches : speedy x86_64 + raspi-srv-2 aarch64) : process, signal, time, ipc, mem, fs, net, security, system, device, ebpf. #19 (brouillon) fermée, branches mergées supprimées. Reste couche 0 : io_uring Temps 2a→4, 3a–3f. Traductions EN des specs reportées (fin de couche 1).

Session 2026-06-11 — Specs device + ebpf : la couche 0 est entièrement spécifiée

Production des deux dernières specs de famille manquantes, au niveau de détail des autres (docs/specs/layer-0/family-device.md, family-ebpf.md), pour permettre une délégation d’implémentation en une passe.

  • device — trois sous-systèmes : socket netlink NETLINK_KOBJECT_UEVENT (UEventSocket RAII + read qui vérifie l’authenticité nl_pid==0/uid==0) avec décodeur UEventMessage emprunté, zéro-alloc (itérateur clé=valeur) ; evdev (lecture typée InputEvent/slice_from_bytes, ioctls dédiés EVIOC*evdev_device_id, evdev_name, evdev_supported_codes, evdev_abs_info, evdev_grab/release, evdev_set_clock…, jamais d’ioctl générique, ADR-021 c.3) ; sysfs explicitement renvoyé à fs (pas de wrapper, anti-duplication). ~22 fonctions. Décision : décoder un format de fil kernel fixe (uevent, input_event) est du miroir légitime en couche 0 (précédent SignalFdInfo) ; le modèle riche de device → couche 1.
  • ebpfcouverture exhaustive de bpf() : les 37 sous-commandes de enum bpf_cmd (cible 6.12) chacune en fonction dédiée typée (cartes + éléments + batch, programmes, attache/liens, épinglage, introspection par id, BTF, stats/jetons), RAII BpfMap/BpfProgram/BpfLink/Btf. Plus perf_event_open (+ PerfEventScope sans sentinelle -1) et ses ~12 ioctls dédiés, dont perf_event_set_bpf_program (le pont eBPF↔perf). On charge des programmes déjà assemblés ; assembleur, chargeur libbpf, BTF/CO-RE, décodage du ring-buffer → couche 1 (aligné sur le précédent seccomp Q4). ~51 fonctions.
  • ABI vérifiée sur en-têtes kernel : bpf (x86_64 321 / aarch64 280), perf_event_open (298 / 241), NETLINK_KOBJECT_UEVENT=15, ioctls EVIOC*, layouts input_event (24 o LP64), input_id, input_absinfo, énumération bpf_cmd.
  • Docs de suivi réconciliées : INDEX.md (tableau + arbre + stats), etat-avancement.md (lignes device/ebpf 📝→🔜, section 3), JOURNAL (cet historique + État courant + « Prochain chantier » réaligné, qui pointait encore par erreur sur ipc).
  • Suite : déléguer l’implémentation de device et ebpf (squelette-doc-d’abord), puis les Temps io_uring restants. Traductions EN des deux specs à produire ultérieurement (comme io_uring).

Session 2026-06-01 (suite, 3g) — mem + net finis : 9 familles couche 0 à 100 % hors exceptions, 2 arches

Dernière passe de couverture. mem et net portées à 100 % hors exceptions (lignes + branches, mesuré artefact-free --workspace --lib --branch), validées sur speedy (x86_64) et raspi-srv-2 (aarch64). Branche feat/layer0-familles-restantes, 4d9dbd8e46e90d.

  • mem (3d802dd) : branches 35,7 → 58,9 % (reste = exceptions). Erreurs réellement atteignables testées (mprotect/madvise/msync EINVAL sur adresse non alignée ; mlock/mlock2/munlock ENOMEM sur intervalle non mappé ; mlockall EINVAL ; memfd_create nom trop long ; process_vm_* ESRCH PID inexistant ; mmap_fixed succès + EEXIST). debug_assert → 2 should_panic. Test flaky mmap_fixed_noreplace_anonymous retiré. Divergence d’environnement mlockall (réussit sur speedy, EPERM sur raspi) documentée en PRIVILEGE.
  • net (e46e90d) : ~30 tests. Erreurs sockets via fd invalide → EBADF (listen/getsockname/getpeername/sendmsg/recvmsg/sockopts) ; socket EPROTONOSUPPORT ; socketpair EOPNOTSUPP ; clamps val > i32::MAX (rcvbuf/sndbuf/linger/keepalive/backlog) ; parseurs raw_to_socket_addr et parse_scm_rights (buffers tronqués/dégénérés) ; passage de FD SCM_RIGHTS bout-en-bout sur socketpair. Reste = exceptions STRUCTURAL (gardes de parseur inatteignables avec données kernel bien formées ; get_so_error val≠0 = erreur socket asynchrone exigeant poll/epoll non wrappés ; if let sur variante déterministe) + FEATURE-KERNEL (IPv6 absent, skip fcntl).

Couche 0 — 9 familles de syscalls à 100 % couverture (lignes + branches) hors exceptions, cross-arch. COVERAGE-EXCEPTIONS.md : 52 entrées (PRIVILEGE 9, FEATURE-KERNEL 12, CHILD-EXIT 8, EFAULT-SAFE 8, STRUCTURAL 11, VALUE-UNREACHABLE 4). Barrière au HEAD verte sur les 2 arches : fmt / clippy -D warnings / cargo test --workspace (371 + 80 + 74 + 2 + intégration). Chiffres bruts llvm-cov (exceptions restant rouges par construction) : lignes ~96,6 %, branches ~78,8 %. io_uring reste le prochain gros chantier couche 0. PR ouverte (non mergée).

Session 2026-06-01 (suite, 3f) — Couverture 100 % : signal/process/ipc/fs/types + artefact llvm-cov élucidé

Reprise du chantier couverture couche 0 (branche feat/layer0-familles-restantes, 77f4e86b6396ce). Objectif : 100 % lignes+branches hors exceptions, mesuré sur les 2 exécuteurs Linux. API gelée (tests seulement).

Découverte méthodologique majeure — artefact [True:0, False:0] de llvm-cov --branch. Les régions de branche fantômes collées sur des lignes pourtant couvertes viennent exclusivement du binaire de test d’intégration (tests/main_thread_invariant.rs), qui lie la lib sans #[cfg(test)] et n’exécute que getpid/gettid : son instanciation [0,0] ne fusionne pas avec les compteurs cfg(test). Preuve : --workspace = 36 shadows sur process ; --workspace --lib = 0 shadow. Décision actée : la barrière branches se mesure cargo +nightly llvm-cov --workspace --lib --branch (artefact-free) ; les lignes restent en --workspace. Documenté en tête de COVERAGE-EXCEPTIONS.md.

4 familles syscall + air-sys-types portées à 100 % hors exceptions (2 arches) :

  • signal (9806f99) : recette flush enfant forké réutilisée (child_exit), EAGAIN/ESRCH ciblés, 2 should_panic, noop_handler, retrait de is_root mort.
  • process (08103a1) : 2 should_panic ; artefacts [0,0] élucidés ; 40 branches restantes ⊆ exceptions.
  • ipc (a7dfe45) : EventFd::write EINVAL, vmsplice/splice-Some EBADF ; retrait d’un test splice buggé + helper mort.
  • fs (c6945ff) : EBADF sur preadv/pwritev/fallocate/copy_file_range/ sync_file_range, symlinkat EEXIST ; retrait du helper mort syscall0.
  • air-sys-types (b6396ce) : réconcilié (résidu = défense-en-profondeur).

Plus : correction de 2 violations fmt --check préexistantes (lot à l’aveugle, d9b126f). COVERAGE-EXCEPTIONS.md : nouvelle catégorie STRUCTURAL, 42 entrées (PRIVILEGE 8, FEATURE-KERNEL 11, CHILD-EXIT 8, EFAULT-SAFE 7, VALUE-UNREACHABLE 4, STRUCTURAL 4).

État au HEAD b6396ce, identique speedy/raspi : fmt/clippy -D warnings/ cargo test --workspace (328+80+74+2) verts. Familles 100 % hors exceptions : time, system, security (préc.) + signal, process, ipc, fs + types complet.

RESTE (2 familles, gaps branches réels mesurés) : mem (branches 35.71 %, 36 rouges) et net (branches 52.59 %, 55 rouges). PR non ouverte (on n’ouvre qu’au 100 % global). Détail : prompt/couche0-familles-restantes.3f.output.md.

Session 2026-06-01 — Passe 2 des 6 familles + validation cross-arch (x86_64 + aarch64)

Implémentation (passe 2) des 6 familles restantes (ipc, mem, fs, net, security, system) et première validation réelle sur deux exécuteurs Linux : speedy (Mac mini Intel, x86_64, Ubuntu 24.04) et raspi-srv-2 (Raspberry Pi 4, aarch64, Ubuntu 26.04). macOS n’est plus un juge (compile seulement ; rate les tests Linux-gated, les doctests exécutés, et les bugs de signedness/arch). Toolchain montée à stable 1.96.0 (MSRV = dernière stable).

Bugs aarch64-spécifiques trouvés au premier passage réel sur aarch64 et corrigés (internes asm!/valeurs, API publique inchangée) :

  • clone3 / hang des tests fork : en exécution parallèle, l’enfant d’un clone3 fork héritait des mutex du harnais multi-thread verrouillés par des threads absents ; std::process::exit (atexit/flush) deadlockait (futex_do_wait). Le wrapper clone3 lui-même est correct (run sériel OK). Fix : sortie enfant via exit_group direct (helper interne #[cfg(test)]).
  • openat / EINVAL sur aarch64 : O_DIRECTORY/O_NOFOLLOW/O_DIRECT/ O_LARGEFILE/O_TMPFILE ont des valeurs différentes entre x86_64 et aarch64 (ABI asm-generic). OpenFlags figeait les valeurs x86_64 → OpenFlags::DIRECTORY désignait O_DIRECT sur aarch64. Fix : valeurs conditionnelles à target_arch (idem PipeFlags/StatusFlags::DIRECT).
  • 7 doctests (jamais exécutés car le Mac faisait cargo doc, pas cargo test --doc) : imports AsFd manquants, GetrandomFlags non re-exporté au niveau crate, exemple SockFprog (E0716). Corrigés.

État vérifié sur les deux arches (HEAD 61868ca) : fmt/clippy -D warnings/build verts ; cargo test --workspace = **196 + 78 + 1 (intégration)

  • 74 + 2 doctests, 0 échec** ; plus aucun hang process:: sur aarch64.

Outillage couverture : nightly dédié installé sur les deux exécuteurs pour la mesure de branches (cargo +nightly llvm-cov --branch) ; le build/CI reste sur stable.

Audit systématique des constantes cross-arch (fait, résultat négatif). Un programme C de référence compilé sur les deux arches a comparé ~270 constantes kernel (O_, MAP_, MADV_, MFD_, SIG*, SA_, SO_/SOCK_/MSG_, SECCOMP_, LANDLOCK_, CLONE_, RLIMIT_, CAP_, PR_, F_SEAL_*, errno…) aux valeurs hardcodées d’air-sys-types. Seule divergence : la famille O_* (O_DIRECTORY/O_NOFOLLOW/O_DIRECT/O_LARGEFILE/O_TMPFILE), déjà corrigée. Tout le reste est identique entre x86_64 et aarch64 — aucune autre correction nécessaire.

Harnais sous-processus seccomp/Landlock (fait, 2/3). seccomp_set_mode_filter (filtre allow-all) et LandlockRuleset::restrict_self sont désormais fonctionnellement testés dans un enfant forké (clone3 + waitid, sortie exit_group), validés sur les deux arches. La mesure de couverture de leur branche succès reste une exception légitime (l’enfant ne réécrit pas son .profraw ; cf. COVERAGE-EXCEPTIONS.md). seccomp_set_mode_strict reste une exception (bloque exit_group + l’I/O du profil).

Reste à faire (chantier couverture, non terminé) : tests fs/mem manquants (fcntl_*, readlinkat, chemins d’erreur), passage à 100 % lignes + branches sur les deux arches, promotion éventuelle d’exit_group en API publique (ADR-021 conv. 5).

Session 2026-05-27 — Famille time complète

Implémentation de la famille time couche 0 dans les deux crates fondatrices :

  • air-sys-types::time : Clock, Instant, SleepDeadline, SleepError, TimerFdFlags, TimerSetFlags, TimerFdSpec.
  • air-sys-syscall::time : clock_gettime, clock_settime, clock_getres, clock_nanosleep, timerfd_create, wrapper RAII TimerFd::{as_fd, into_fd, arm, disarm, current, read}.
  • Errno::EAGAIN ajouté pour couvrir les lectures non bloquantes sans événement disponible.
  • Tests ajoutés : chemins monotonic/resolution/sleep relatifs, validation cross-clock pour clock_nanosleep, lecture non bloquante timerfd, armement one-shot, round-trip current/disarm.

Résultat : cargo test -p air-sys-types -p air-sys-syscall vert le 2026-05-27, soit 165 tests passants au total.

Session 2026-05-24 (suite) — Famille signal complète

Branche feat/layer-0-family-signal. Implémente la famille signal couche 0 conformément à docs/specs/layer-0/family-signal.md et ADR-020 (signalfd par défaut + sigaction restreint aux 4 signaux synchrones fatals) :

  • Sous-section 1 (signalfd + masques) : signalfd_create, signalfd_create_blocking, SignalFd::{read, update_mask, as_fd, into_fd, from_owned_fd}, block_signals, unblock_signals, set_signal_mask, current_signal_mask, wait_for_signal.
  • Sous-section 2 (envoi) : kill, tgkill, rt_sigqueueinfo.
  • Sous-section 3 (sigaction restreint) : sous-module synchronous_handler avec install_fatal_handler et restore_handler (unsafe), restreints à FatalSignal::{Segv, Bus, Fpe, Ill}.

Dette d’ambiguïté SignalInfo vs SignalQueueInfo — fermée. La PR pidfd_* avait consigné l’ambiguïté entre les deux types (family-process.md nommait SignalInfo, family-signal.md nommait SignalQueueInfo pour ce qui était fondamentalement le même siginfo_t côté kernel). Résolution dans cette PR : un seul type SignalInfo porte le concept ; SignalQueueInfo disparaît.

  • SignalInfo reste opaque côté représentation (#[repr(C)] struct { raw: [u8; 128] }, layout siginfo_t Linux) — sa représentation est inchangée pour ne pas casser pidfd_send_signal qui passe son pointeur au kernel as-is.
  • SignalInfo gagne un constructeur public new_queue(value: SignalValue) qui fixe si_code = SI_QUEUE (-1), seule valeur que le kernel accepte sans drapeau privilégié pour un envoi cross-process userspace (cf. do_rt_sigqueueinfo dans kernel/signal.c : « Not even root can pretend to send signals from the kernel »). Le champ si_signo est laissé à zéro — le kernel l’écrase avec sig à la délivrance.
  • SignalValue { Integer(i32), Pointer(u64) } est promu type public, conforme spec.
  • rt_sigqueueinfo(pid, signal, info: &SignalInfo) consomme la même API.

Déviation mineure de spec assumée. family-signal.md documentait rt_sigqueueinfo(..., info: SignalQueueInfo) par valeur avec champs publics { code: i32, value: SignalValue }. La présente implémentation passe &SignalInfo (référence) construit via new_queue (constructeur discipliné). Raison de sécurité : exposer code brut serait un footgun — le kernel rejette les si_code arbitraires en envoi cross-process avec EPERM. Exposer un constructeur discipliné évite que l’utilisateur ne fabrique un siginfo_t qui se ferait rejeter, ou pire qui « falsifierait » l’origine du signal. La spec sera mise à jour à froid (hors périmètre PR).

pidfd_send_signal non modifié. Périmètre strict respecté : pidfd_send_signal (PR process déjà mergée) n’est PAS touché. Son trou de couverture Some(_) (3 lignes) reste acté. Si un test légitime de cette PR l’avait appelé en Some(...), le trou se serait fermé naturellement — ce n’était pas un objectif et ce n’est pas arrivé (aucun test de cette PR ne fait appel à pidfd_send_signal). Diff vérifié : crates/air-sys-syscall/src/process.rs n’est pas dans la liste des fichiers modifiés.

Constantes Signal ajoutées — 4, vérifiées vs asm-generic/signal.h. SIGILL=4, SIGBUS=7, SIGFPE=8, SIGUSR1=10. Identiques x86_64/aarch64 (ces archs incluent asm-generic/signal.h ; les archs comme MIPS/SPARC/Alpha qui surchargent sont hors ADR-014). Signal total après cette PR : 10/~31 standards POSIX, toujours stub partiel documenté en tête de module.

Borne structurelle Signal::try_from_raw ∈ [1, 64] — correction Principe 4. Pendant la phase de contrôle, défaut identifié : Signal::try_from_raw validait seulement > 0, autorisait donc Signal(99999) qui se serait fait rejeter par EINVAL au runtime côté kernel. Violation du Principe 4 (validation amont) : Signal doit porter sa garantie de validité dans le type, pas attendre le kernel.

Corrections appliquées dans cette PR (périmètre légitime — Signal EST le type central de la famille signal que cette PR complète déjà) :

  • Signal::try_from_raw(raw) rejette désormais aussi raw > 64 (= _NSIG Linux). Renvoie None pour tout raw ∉ [1, 64].
  • Signal::from_nonzero (qui ne validait pas la borne) retiré : aucune utilisation hors du test roundtrip lui-même, simplification de l’API publique. Les constantes internes (SIGUSR1, etc.) passent par Self(unwrap_nz(N)) qui ne touche pas à from_nonzero.
  • unwrap_nz interne renforcé : panique au compile-time si une constante future est définie avec une valeur hors [1, 64]. Défense en profondeur ; toute régression d’invariant détectée à la compilation.
  • Constante NSIG: i32 = 64 exposée au module (privée, mais documentée).
  • 4 nouveaux tests : try_from_raw_accepts_lower_bound_1, try_from_raw_accepts_upper_bound_nsig, try_from_raw_rejects_above_nsig (65, 99999, i32::MAX), unwrap_nz_runtime_call_panics_above_nsig (65, 100).

Conséquence couverture — 4 lignes acquittées « défense en profondeur ». Avant la borne : « no-op silencieux pour signal > 64 » — comportement faible (silently ignore une demande explicite). Après la borne : défense en profondeur derrière une validation amont — un Signal valide ne peut structurellement pas avoir une valeur > 64 ; les branches None restent comme garde par Principe 5 (sur-sécuriser, ne jamais corrompre) au cas où un futur constructeur oublierait la borne. unreachable!() refusé : transformerait une garde silencieuse en panic potentiel, ce qui est précisément ce que Principe 5 nous demande d’éviter.

Les 4 lignes non couvertes en line-coverage dans air-sys-types/src/signal.rs :

  • 3 lignes : branches None => false / None => self / None => self de SignalMask::{contains, with, without} — défensives, inatteignables tant que Signal::try_from_raw borne à [1, 64].
  • 1 ligne : bras None => panic!("unreachable: n > 0 guaranteed by the check above") dans unwrap_nz — inatteignable parce que la garde n <= 0 || n > NSIG au-dessus filtre déjà tout argument zéro avant que NonZeroI32::new soit appelé. Conservé pour satisfaire l’exhaustivité du match const ; même statut « défense en profondeur ».

Toutes catégorie « défense en profondeur, structurellement inatteignable derrière borne validée » — justification cette fois solide (la borne EST garantie par le type).

Vérification non-régression famille process. La borne ≤ 64 ne casse aucun usage existant : tous les signaux utilisés dans family-process sont dans [4, 19] (SIGCHLD=17, SIGKILL=9, SIGSEGV=11, SIGSTOP=19, SIGTRAP=5, SIGABRT=6, SIGBUS=7, SIGFPE=8, SIGILL=4). Les 92 tests air-sys-syscall passent inchangés sur 3 runs stress successifs après l’introduction de la borne.

Stratégie de test pour l’état global signal — mise en œuvre.

  • MaskGuard RAII (test-only, mod tests) : capture le mask courant en toute première ligne de chaque test, restaure intégralement via SIG_SETMASK (jamais SIG_BLOCK/UNBLOCK relatif) à la destruction. Couvre la contamination per-thread (cargo test recycle les workers).
  • tgkill(self_pid, self_tid, sig) pour les tests d’envoi/réception intra-process : cible exactement le thread courant, évite tout routage à un worker collatéral cargo. Combiné avec signalfd_create_blocking, donne une livraison déterministe sans flakiness.
  • Fork-enfant mono-thread pour les opérations process-wide ou unsafe :
    • kill (cible PID, pas TID — risque de router vers un worker tiers en multi-thread).
    • rt_sigqueueinfo (idem, cible PID).
    • install_fatal_handler / restore_handler (handlers PROCESS-WIDE, contamineraient les autres tests).
  • Aucun test ne déclenche réellement un signal fatal (SIGSEGV via ud2, etc.) — l’install/restore est validé par round-trip, le déclenchement effectif reste hors périmètre (crash reporter end-to-end est un cas family-security / future PR).

Aucune nouvelle dépendance externe. Tous les types kernel (signalfd_siginfo 128 octets, sigset_t u64, struct sigaction 24/32 octets selon arch) sont décrits en #[repr(C)] directement. core::ffi::{c_int, c_void} couvre l’ABI des handlers.

Tests bout-en-bout. 37 tests dans air-sys-syscall::signal (mask round-trip, signalfd round-trip, wait_for_signal bloquant, tgkill+signalfd, kill en fork-enfant, rt_sigqueueinfo en fork-enfant pour les deux variantes SignalValue, ESRCH, install/restore en fork-enfant, parsing signalfd_siginfo unit, errno helper local, wait_for_signal timeout-non-supporté). 10 tests dans air-sys-types::signal (4 nouvelles consts, SignalInfo new_queue avec Integer et Pointer + offsets vérifiés sur as_bytes, SignalMask helpers, SignalFdFlags, FatalSignal mapping).

Stress test : 5 runs successifs de cargo test --workspace, 0 échec. La stratégie tgkill+fork pour les opérations process-level neutralise complètement les races multi-thread observées en début d’itération (signal: 10 sur un worker collatéral).

Métriques finales. 92 tests dans air-sys-syscall (22 nouveaux pour signal) + 58 dans air-sys-types (13 nouveaux pour signal, dont 4 de bornes ajoutés en phase de contrôle finale) + 1 intégration. Total 150 tests passants. Couverture production : air-sys-types::errno/signal/process 100 % fonctions ; air-sys-types::signal 98.3 % lignes (4 lignes catégorie « défense en profondeur » détaillées ci-dessus) ; air-sys-syscall 94-95 % lignes avec gaps catégorisés (mêmes catégories que famille process : Err paths via helper commun déjà testé, branches Ok inatteignables sans privilèges, exits enfants fork-pattern, skips AppArmor/Yama).

Session 2026-05-24 — Fin de la famille process : groupes/sessions, prctl, rlimits, capabilities

Branche feat/layer-0-process-complete. Implémentation des sous-sections 4 à 7 de docs/specs/layer-0/family-process.md, complétant la famille process en couche 0 :

  • Sous-section 4 (groupes/sessions) : setpgid, getpgid, setsid, getsid. Convention 1 ADR-021 appliquée — Option<Pid> partout, jamais la sentinelle kernel 0.
  • Sous-section 5 (prctl par opération) : 12 fonctions individuelles typées (set/get_parent_death_signal, set/get_no_new_privs, set/get_thread_name, set/get_dumpable, set/get_keep_caps, set_timer_slack, cap_ambient_raise/lower/clear_all/is_set). Convention 3 ADR-021 strictement appliquée : helper privé raw_syscall_prctl non exposé, chaque opération est sa propre fonction publique typée.
  • Sous-section 6 (rlimits) : getrlimit, setrlimit, prlimit — implémentation pivot sur prlimit64 (syscall moderne, atomique, ciblable). getrlimit/setrlimit deviennent des alias de compatibilité qui délèguent à prlimit(None, ...).
  • Sous-section 7 (capabilities) : capget, capset via Linux v3 capabilities (header + 2× __user_cap_data_struct). Conversion u64 ↔ (lo, hi) confinée à air-sys-syscall ; CapabilityMask côté air-sys-types reste un type pur.

Capability — stub partiel à 20 variantes. Création de l’enum Capability avec les 20 variantes listées par la spec (sous-section 7), discriminants explicites tirés de include/uapi/linux/capability.h et vérifiés un-à-un dans un test (capability_discriminants_match_uapi_linux_capability_h). CAP_SYS_PACCT = 20 est délibérément absent (pas listé par la spec) — aucune confusion possible avec CAP_SYS_ADMIN = 21. Les ~21 capabilities restantes (CAP_SETPCAP, CAP_LINUX_IMMUTABLE, CAP_NET_BROADCAST, CAP_IPC_LOCK/OWNER, CAP_SYS_PACCT, CAP_SYS_TTY_CONFIG, CAP_MKNOD, CAP_LEASE, CAP_AUDIT_, CAP_MAC_, CAP_SYSLOG, CAP_WAKE_ALARM, CAP_BLOCK_SUSPEND, CAP_PERFMON, CAP_BPF, CAP_CHECKPOINT_RESTORE, …) seront ajoutées au fil des PRs qui les utiliseront. Le commentaire de tête de l’enum le documente.

CapabilityMask u64 ↔ split (u32, u32) — test du placement absolu. Les Linux v3 capabilities exposent leur bitfield via deux mots u32 côté ABI (datap[0] = bits 0-31, datap[1] = bits 32-63). Une éventuelle inversion lo/hi dans mask_to_words se compenserait dans un simple round-trip via words_to_mask — le bug serait invisible. D’où trois tests d’absolu : mask_to_words_places_bit_0_in_lo, mask_to_words_places_bit_32_in_hi, words_to_mask_recombines_with_correct_placement. Vérifient le placement exact d’un bit dans chaque mot, sur valeur brute (1_u64 << 32) — indépendamment des variants Capability disponibles. Aucune Capability de ce stub n’occupe le mot haut (max = SysTime = 25) ; quand la PR ultérieure ajoutera SetFCap = 31 puis MacOverride = 32, le mot haut sera exercé via une capability réelle.

Aucun ajout Errno ni Signal. Les 6 errnos et 6 signaux du stub partiel actuel suffisent (vérifié via croisement spec↔code). Discipline « ne pas pré-implémenter sans appelant » respectée.

Vérification des numéros de syscall vs spec — aucune divergence. setpgid 109/154, getpgid 121/155, setsid 112/157, getsid 124/156, prctl 157/167, getrlimit 97/163, setrlimit 160/164, prlimit64 302/261, capget 125/90, capset 126/91. Tous concordent ; la spec n’indiquait pas le numéro de prctl (157 x86_64 / 167 aarch64 dans linux/syscalls.h).

Allocation heap justifiée par convention 4 ADR-021. Une seule fonction de cette PR alloue dans son happy-path : get_thread_name() -> CString. Justification : kernel écrit dans un buffer transient de 16 octets ; la spec exige un type owned côté caller (peut sortir du frame). Allocation bornée à 16 octets (un seul Box<[u8]>), exception explicitement admise par la convention 4. capget ne fait pas d’allocation heap : CapabilitySet (3 × u64 = 24 octets) est retourné par valeur sur la pile, le buffer kernel intermédiaire (KernelCapData[2] = 24 octets) est aussi pile.

Mitigation d’une flakiness pré-existante. Le test waitid_dispatches_processgroup_and_anyprocessgroup (PR clone3+waitid) appelait waitid(AnyProcessGroup | EXITED | NOHANG) en parallèle des tests qui forkent. Sans NOWAIT, cet appel pouvait consommer un événement appartenant à un autre test parallèle (canonical SIGKILL test, fork tests de cette PR), provoquant un ECHILD chez le test légitime. Mitigation appliquée dans cette PR : ajout de WaitOptions::NOWAIT (peek-only) au test dispatch. Avant : 1/N failure rate ; après : 5 runs stress successifs sans failure.

Tests skippés en environnement dev — récap consolidé. À l’issue de cette PR, trois conditions runtime Ubuntu 24.04 par défaut masquent silencieusement des chemins applicatifs :

  1. AppArmor (apparmor_restrict_unprivileged_userns=1) — bloque clone3_with_new_user_namespace_runs_or_skips.
  2. Yama (yama.ptrace_scope ≥ 2) — bloquerait pidfd_getfd_duplicates_fd_from_child (Ubuntu 24.04 par défaut = 1, OK).
  3. Non-root + non-pgrp-leader runner cargo — neutralise le scénario EPERM direct de plusieurs tests (setsid, capset_raise_non_permitted, setrlimit_raising_hard, cap_ambient_raise_non_permitted, clone3_newpid_without_caps). La majorité ont été mitigés via scénarios fork (l’enfant fork’é se rend pgrp leader, puis exerce le chemin EPERM côté setsid ; les autres restent skip-on-root).

Validation effective de ces chemins relève d’un runner CI permissif (sans AppArmor userns restriction, avec ptrace_scope ≤ 1, et idéalement avec un harness cargo test qui rend le test runner pgrp leader). À provisionner avec le pipeline CI.

Trous de couverture restants — catégorisés. Sur ~1960 lignes production dans air-sys-syscall/process.rs, ~25 lignes restent non couvertes après cette PR. Aucune ne révèle un bug ou un risque :

  • 3 lignes : branche Some(&info) de pidfd_send_signal (stub SignalInfo opaque, acted PR pidfd_*).
  • 10 lignes : corps complet de set_no_new_privs. Testable en enfant fork’é dès aujourd’hui (clone3+waitid sont mergés ; l’enfant l’appelle, vérifie via get_no_new_privs, exit code ; parent waitid). Choix de regroupement, pas une impossibilité : le test bout-en-bout sera écrit dans la PR family-security (seccomp) qui constitue le contexte naturel d’usage de NNP — les deux fonctions partagent les mêmes fixtures fork-enfant et la même discipline de scénario.
  • ~12 lignes : branches d’erreur (if ret < 0 { return Err(errno_from_negative_syscall_ret(ret)) }) de wrappers prctl simples (set/get_parent_death_signal, get_no_new_privs, set/get_thread_name, set/get_dumpable, set/get_keep_caps, set_timer_slack, cap_ambient_lower/clear_all/is_set) et de capget. Ces opérations n’échouent pas en pratique sur un kernel sain ; leur site Err est inatteignable sans injection de kernel-failure. La logique de décodage Err elle-même est intégralement couverte via le helper commun errno_from_negative_syscall_ret (un point unique appelé identiquement par tous ces sites), testé en isolation par errno_from_negative_syscall_ret_maps_known_codes sur 6 valeurs négatives synthétiques (-1, -3, -4, -9, -10, -22EPERM, ESRCH, EINTR, EBADF, ECHILD, EINVAL). Seuls les sites d’appel restent non exercés en line-coverage ; la sémantique d’erreur est validée centralement. Aucun risque de logique d’erreur jamais validée.
  • ~1 ligne : Ok(()) de cap_ambient_raise — la fonction ne SUCCÈDE que si l’appelant a cap dans permitted∩inheritable. En non-root standard, c’est jamais. Couvrable seulement avec un binaire ayant des file capabilities pré-positionnées.
  • ~2 lignes : variante SuidDumpable du décodage get_dumpable (valeur kernel 2, dépréciée — kernels modernes retournent EINVAL sur PR_SET_DUMPABLE=2, donc le décodeur n’a jamais à la traiter).

Documentation au code (commentaires explicites) + ce JOURNAL = trace complète. Pas de tricherie sur les chiffres.

Pattern admis transversalement — exits d’échec côté enfant dans les tests fork. À partir de cette PR, le pattern « enfant fork’é encode son résultat de round-trip par code de sortie » (if X.is_err() { std::process::exit(N); } pour N ≠ 0, succès = exit(0)) est acté comme convention de test. Les branches exit(N) d’erreur ne sont prises que si la round-trip échoue côté enfant — c’est-à-dire jamais sur un environnement sain. Ces lignes apparaîtront systématiquement en non-couvertes par llvm-cov, par construction. À ne plus comptabiliser comme dette de couverture à chaque PR : c’est le coût normal d’un pattern de test fork-enfant robuste, supérieur à un test in-process qui contaminerait l’état partagé. Si la round-trip échoue en pratique, la branche s’exécute, le compteur s’incrémente, et la couverture remonte — c’est un canari, pas un trou.

Métriques finales. 70 tests dans air-sys-syscall (38 nouveaux), 45 dans air-sys-types (9 nouveaux), 1 binaire d’intégration inchangé — total 116 tests passants. Coverage production : 100 % air-sys-types, ~93 % air-sys-syscall (130 lignes manquées dont 30 production catégorisées en (A)/(B)/(C)/(D)/(E) ci-dessus + 100 lignes test = skips environnementaux + exits d’échec enfant + branches signal-induced). Régions à 89 % global, conformément à la politique JOURNAL antérieure (régions < 100 % acceptées sur branches d’invariants par construction et skips environnementaux).

Session 2026-05-23 — pidfd_open + pidfd_send_signal + pidfd_getfd

Troisième PR couche 0, sur la branche feat/layer-0-pidfd. Implémentation des trois wrappers pidfd_* de la famille process (spec docs/specs/layer-0/family-process.md sous-section 3) :

  • pidfd_open(pid, flags) -> Result<PidFd, Errno> — syscall 434 (x86_64 et aarch64), Linux 5.3+.
  • pidfd_send_signal(pidfd, signal, info) -> Result<(), Errno> — syscall 424, Linux 5.1+.
  • pidfd_getfd(pidfd, target_fd, flags) -> Result<OwnedFd, Errno> — syscall 438, Linux 5.6+.

PidFd est réutilisé (existe déjà depuis la PR clone3, qui le retournait dans CloneResult::Parent via CLONE_PIDFD) ; aucune réimplémentation. Drop hérité de OwnedFd — fermeture du fd à la destruction du PidFd, sans impl Drop explicite (qui aurait été redondant et aurait pu interdire les move-out de into_fd). Le test pidfd_drop_closes_underlying_fd prouve cette fermeture en post-mortem via waitidEBADF sur un RawFd re-borrowé après drop.

Ajouts dans air-sys-types :

  • PidFdOpenFlags (bitflags u32) : une seule valeur, NONBLOCK = 0x800, conforme au seul drapeau valide en kernel actuel.
  • SignalInfo (stub opaque) : #[repr(C)] pub struct SignalInfo { _opaque: [u8; 128] } dans air-sys-types::signal. Aucun constructeur public — le type est type-reachable mais value-unreachable depuis l’extérieur de la crate. La signature pidfd_send_signal(..., info: Option<&SignalInfo>) reste strictement conforme à la spec ; seul None est utilisable. Test interne signal_info_size_matches_linux_siginfo_t ancre la taille à 128 octets (ABI kernel).
  • Errno::EBADF (9), Errno::ESRCH (3) : deux nouvelles constantes, chacune référencée par un test (drop closure, PID inexistant). Le total Errno reste un stub partiel (6 constantes sur ~140), conformément à la discipline établie dans la PR précédente : on ajoute uniquement ce qui a un appelant.

Vérification des valeurs Errno vs asm-generic/errno-base.h. Les six constantes (EPERM=1, ESRCH=3, EINTR=4, EBADF=9, ECHILD=10, EINVAL=22) sont définies dans asm-generic/errno-base.h et utilisées identiquement par x86_64 et aarch64. Aucune surcharge dans asm/errno.h pour ces deux architectures (les surcharges existent sur Alpha/MIPS/SPARC/ParISC, hors ADR-014). Vérifié à la rédaction.

Dette terminologique signalée — SignalInfo vs SignalQueueInfo. La spec family-process.md (sous-section 3, pidfd_send_signal) nomme le paramètre info: Option<&SignalInfo> ; la spec family-signal.md (sous-section 2, rt_sigqueueinfo) introduit SignalQueueInfo avec champs structurés (code: i32 + value: SignalValue). Côté kernel, c’est pourtant le même siginfo_t qui passe dans les deux syscalls. Cette ambiguïté terminologique entre les deux specs doit être harmonisée en PR family-signal (renommer, fusionner, ou clarifier explicitement les deux types — par ex. SignalInfo = vue opaque côté ABI ; SignalQueueInfo = builder Air ergonomique au-dessus). Cette PR-ci ne tranche pas et laisse SignalInfo opaque pour éviter de figer son design.

Trou de couverture acté — branche Some(&info) de pidfd_send_signal. Les lignes 742-744 (extraction du pointeur depuis &SignalInfo) restent non couvertes en ligne, parce que SignalInfo n’a aucun constructeur public dans cette PR — la branche est type-reachable, value-unreachable depuis l’extérieur. Refus délibéré d’introduire un constructeur pub(crate) ou un SignalInfo::zeroed() juste pour la couverture : ce serait commencer à donner du contenu au stub, contre la décision adoptée pour SignalInfo opaque, et un test avec SignalInfo zéroé serait un faux test (validerait « ça ne plante pas », pas que info est transmis correctement — le kernel pourrait même rejeter un si_code incohérent). Le chemin None est testé bout-en-bout par le test canonique SIGKILL — c’est suffisant pour cette PR. // TODO(family-signal): tester Some(&info) avec un vrai constructeur est porté par le commentaire de la fonction. Couverture ré-atteinte 100 % production dès que la PR family-signal apporte le builder public.

Scénario canonique « parent tue enfant » enfin implémenté. La PR clone3 avait posé un waitid_decodes_child_killed_by_signal qui utilisait l’auto-abort() côté enfant (équivalent kernel pour le décodage siginfo, mais non représentatif de l’usage réel). Cette PR ajoute pidfd_send_signal_canonical_parent_kills_child_with_sigkill : parent fork (CLONE_PIDFD), enfant entre en boucle sleep, parent pidfd_send_signal(pidfd, SIGKILL, None), waitid observe Killed { signal: SIGKILL, .. }. C’est l’orchestration réellement employée en production. Le commentaire de tête de waitid_decodes_child_killed_by_signal a été actualisé (sans réécriture du test) pour pointer vers le scénario canonique au lieu d’un TODO(family-signal). La ligne loop { sleep } côté enfant du nouveau test reste non couverte : SIGKILL est uncatchable, donc aucun atexit handler ne s’exécute côté enfant ; même limitation que std::process::abort(), déjà actée précédemment. Trou test-code, pas production.

Skip environnement-dépendant — Yama ptrace_scope. Le test pidfd_getfd_duplicates_fd_from_child requiert que le kernel autorise le ptrace parent → enfant. Sur les systèmes où /proc/sys/kernel/yama/ptrace_scope ≥ 2, le kernel refuse même cette relation (CAP_SYS_PTRACE requis). Le test détecte cette condition au runtime et skip proprement avec un eprintln! explicite, à la même manière que le skip AppArmor pour CLONE_NEWUSER côté clone3. Aucune ligne production n’est dépendante de cette condition — la couverture production reste équivalente.

Tests skippés en environnement dev Ubuntu 24.04 — exécution réelle relève de la CI sur runner permissif. À l’issue de cette PR, deux restrictions runtime Ubuntu 24.04 par défaut peuvent silencieusement masquer du chemin réel :

  • AppArmor (kernel.apparmor_restrict_unprivileged_userns=1) bloque le test clone3_with_new_user_namespace_runs_or_skips (PR clone3+waitid). Sans privilèges et sans levée AppArmor, la création de CLONE_NEWUSER échoue avant fork : le test ressort en SKIP, son corps n’est pas exécuté.
  • Yama (kernel.yama.ptrace_scope ≥ 2) bloquerait le test pidfd_getfd_duplicates_fd_from_child (cette PR). Sur Ubuntu 24.04 la valeur par défaut est 1 (parent → enfant autorisé), donc le test s’exécute en pratique ; mais sur tout système avec ≥ 2 il passerait en SKIP.

Conséquence pour le suivi qualité : « cargo test passe 70/70 » sur cette machine ≠ « tous les chemins applicatifs sont validés ». Les chemins NEWUSER et pidfd_getfd-via-ptrace ne sont pas exercés sur un dev Ubuntu 24.04 stock. Leur validation effective relève d’un runner de référence en CI configuré sans ces restrictions (image dédiée, capabilities ajustées, ou simplement userns activés et yama.ptrace_scope=0/1). À provisionner quand le pipeline CI sera mis en place ; en attendant, le BDFL est conscient que ces deux tests sont en lecture seule sur sa machine.

Tests ajoutés et couverture obtenue. Sept tests pour les pidfd_* :

  1. pidfd_open_returns_pidfd_usable_for_waitid_over_child_lifetime — happy path bout-en-bout, prouve aussi que le pidfd survit à la mort de l’enfant.
  2. pidfd_open_returns_esrch_for_nonexistent_pid — branche ESRCH, PID 0x7FFF_FFFE au-delà de PID_MAX_LIMIT (~2^22).
  3. pidfd_drop_closes_underlying_fd — preuve RAII, avec caveat documenté sur la flakiness en multi-thread (mitigée par le fait qu’aucun code n’est exécuté entre drop et la vérification EBADF).
  4. pidfd_send_signal_on_closed_pidfd_returns_ebadf — chemin d’erreur de pidfd_send_signal.
  5. pidfd_send_signal_canonical_parent_kills_child_with_sigkill — scénario canonique SIGKILL parent → enfant.
  6. pidfd_getfd_duplicates_fd_from_child — happy path avec skip Yama si ptrace_scope ≥ 2.
  7. pidfd_getfd_on_closed_pidfd_returns_ebadf — chemin d’erreur de pidfd_getfd.

Couverture finale : production 100 % lignes hors les 3 lignes Some(_) de pidfd_send_signal, qui sont le trou délibéré du stub SignalInfo (acté ci-dessus). Régions à 95 % global. Les 35 lignes restantes non couvertes sont toutes en mod tests (skips environnement + branches signal-induced exits déjà actées).

Session 2026-05-22 (suite) — clone3 + waitid, et première exception dépendance

Deuxième PR couche 0 sur la branche feat/layer-0-clone3-waitid. Implémentation des wrappers clone3 (unsafe, fork-style) et waitid (safe) de la famille process, et de la matière de types associée dans air-sys-types : CloneFlags (bitflags), CloneArgs, CloneResult, StackSpec, PidFd, WaitTarget, WaitOptions, WaitStatus, WaitEvent, plus minimum vital pour Errno, Signal, et réexport std::os::fd::{OwnedFd, BorrowedFd, RawFd}.

Périmètre de cette PR sur CloneArgs. Trois champs exposés (flags, exit_signal, stack) — la spec couche 0 (docs/specs/layer-0/family-process.md) en liste neuf. Les six restants (pidfd, child_tid, parent_tid, tls, set_tid, cgroup) seront ajoutés au fil des syscalls qui les motivent (notamment execve pour stack, namespaces de mount pour set_tid). L’évolution est strictement additive ; aucun champ existant ne sera renommé ni retiré. La demande de pidfd se fait via CloneFlags::PIDFD — le PidFd résultant est retourné dans CloneResult::Parent. La création de thread (CLONE_VM/CLONE_THREAD/CLONE_SIGHAND ou stack: Some(_)) est refusée explicitement avec Errno::EINVAL tant que les tests bout-en-bout ne la couvrent pas.

Première exception au Principe 6 — bitflags. L’utilisation de la crate bitflags (~30+ types de drapeaux Linux à venir dans la couche 0) ne respecte pas la règle des 80 % du Principe 6 : la macro bitflags! émet pour chaque type un grand nombre de traits et de méthodes (BitOr/BitAnd/BitXor/Not/Sub/Extend/FromIterator/iter/from_bits/…) dont Air n’utilise qu’une fraction. Le ratio se mesure sur le code émis, pas sur la « surface d’API utile ». Adoption par exception explicite (modèle ADR-016 / icu4x), inscrite dans EXCEPTIONS.md à la racine. Distinction importante consignée pour qu’elle ne se vide pas par mesure subjective : DEPENDENCIES.md reste réservé aux dépendances qui passent le seuil ; les autres vivent dans EXCEPTIONS.md, jamais par redéfinition du dénominateur.

Pin strict appliqué localement sur bitflags. Cette PR pose bitflags = "=2.11.1" (et non "^2.4" / "2.4") dans [workspace.dependencies]. Le pin strict apporte deux garanties complémentaires au Cargo.lock committé : (1) le lock garantit la reproductibilité du build tant qu’il est intact (ADR-025) ; (2) le pin strict ajoute la reproductibilité de la résolution — si le lock est supprimé ou qu’une re-résolution est forcée (ajout d’une autre dép qui contraint le solver, cargo generate-lockfile), une borne lâche ^2.4 peut glisser silencieusement vers la plus récente 2.x compatible, ce qu’=2.11.1 interdit. Le pin reste cohérent entre Cargo.toml, Cargo.lock, et EXCEPTIONS.md. Question délibérément non tranchée ici : politique transversale (=strict pour toute la couche 0/1 ? ^caret au-delà ? autre découpage ?). Cette décision touche aux documents fondateurs (phase-B1, ADRs) et n’a pas sa place dans une PR de wrapper syscall. À instruire dans un ADR ou un futur DEPENDENCY_POLICY.md dédié, où elle sera revue à froid pour ce qu’elle est : une politique transversale, pas un détail technique de clone3.

Errno et Signal adoptés en stubs partiels. Cette PR n’inscrit dans air-sys-types que les constantes effectivement référencées par son code et ses tests : Errno::{EPERM, EINTR, ECHILD, EINVAL} (4 sur les ~140 prévues par ADR-019) et Signal::{SIGCHLD, SIGABRT, SIGKILL, SIGSEGV, SIGSTOP, SIGTRAP} (6 sur les ~31 POSIX standards de family-signal.md). Le périmètre complet — autres constantes, méthodes name()/description() pour Errno, SignalMask/SignalFdInfo pour Signal — viendra dans les PRs dédiées (ADR-019 complet pour Errno, famille signal pour Signal). Discipline assumée : ne pas pré-implémenter des constantes sans appelant ici, sous peine de figer leur design hors de leur PR et de créer du code mort. Le commentaire de tête de chaque module le note explicitement pour qu’un futur contributeur ne tombe pas dans le piège « tant qu’à faire, j’ajoute les autres signaux pendant que j’y suis ».

Stratégie de coverage post-fork. cargo-llvm-cov configure LLVM_PROFILE_FILE avec un placeholder %p ; chaque processus (parent et chaque enfant forké) écrit son propre .profraw, fusionnés à l’agrégation. Conséquence : l’enfant qui termine par std::process::exit(N) (atexit handlers exécutés, profile flushé) contribue à la couverture du wrapper. L’enfant qui termine par std::process::abort() (SIGABRT) ou un signal-induced exit ne flush PAS son profil — limitation acceptée.

Trous de couverture connus et assumés (sur le périmètre de cette PR) :

  • Variants WaitEvent::Stopped, WaitEvent::Continued, WaitEvent::Trapped : couverts uniquement par le helper privé siginfo_to_wait_event testé sur des SigInfo synthétisés en mémoire. Le remplissage effectif de siginfo_t par le kernel pour ces variants (CLD_STOPPED/CLD_CONTINUED/CLD_TRAPPED) n’est PAS testé bout-en-bout. Trois scénarios manquent : (a) SIGSTOP/SIGTSTP envoyé à un enfant + waitid(WSTOPPED), (b) SIGCONT envoyé après reprise + waitid(WCONTINUED), (c) ptrace-trap + waitid côté tracer. Si un futur bug surgit sur la conversion si_code → variante pour l’un de ces trois cas, regarder en priorité le helper siginfo_to_wait_event et ajouter le scénario bout-en-bout manquant — l’absence de test n’équivaut pas à une absence de risque.

  • Test exec dans l’enfant (5ᵉ test clone3 de la spec) : déféré à la PR execve. Sans wrapper Air d’execve, on ne peut pas tester la séquence fork+exec. Le commentaire au-dessus du module de tests le note.

  • Test namespaces CLONE_NEWUSER skippé : sur Ubuntu 24.04+ (et tout système où kernel.apparmor_restrict_unprivileged_userns=1), le test passe par un eprintln!("SKIP …") et un return. Les ~20 lignes du corps du test ne sont pas exécutées dans cet environnement. Le wrapper clone3 lui-même n’a aucun branchement spécifique sur les bits CLONE_NEW*, donc la couverture production n’est pas réduite — c’est uniquement le test environnement-dépendant qui est en lecture seule sur cet environnement. Re-exécuter ce test sur un système qui autorise les userns non privilégiés (ex. Fedora Workstation, conteneur dédié) fermerait les lignes correspondantes.

  • Test capabilities (NEWPID sans NEWUSER) : branche SKIP root non couverte. Le test contient un garde if is_root() { eprintln!(...); return; }. Quand le runner n’est pas root (cas habituel), les lignes du SKIP ne sont pas exécutées. Si jamais le runner tourne en root, le scénario « EPERM attendu » ne s’applique pas et le SKIP entre en jeu ; sinon le corps du test couvre la branche Err(EPERM).

  • Test waitid_decodes_child_killed_by_signal — orchestration canonique différée. Le test fait s’auto-abort() l’enfant (production de SIGABRT côté enfant), ce qui donne siginfo identique à « parent → child via kill/pidfd_send_signal » : si_code = CLD_KILLED/CLD_DUMPED, si_status = SIGABRT. La branche du dispatch waitid exercée est la même — l’objet du test (validation du décodage en WaitEvent::Killed) est rempli. Ce que ce test ne prouve pas : l’orchestration parent→enfant réellement employée en production (où le parent contrôle la mort de l’enfant). Cette orchestration exige un wrapper kill ou pidfd_send_signal, qui appartient à la famille signal et n’est pas dans cette PR. Option refusée délibérément : écrire ~40 lignes d’asm! test-only pour un raw_kill était tentant mais aurait été un syscall kill réimplémenté hors périmètre, sans SAFETY revues, sans conventions ADR-021 — exactement le débordement qu’on a corrigé sur Errno/Signal. Le test garde une branche // TODO(family-signal): qui le pointera vers le scénario canonique quand le wrapper existera.

  • Trou de couverture résiduel sur l’auto-abort() côté enfant. std::process::abort() (et SIGKILL si on avait choisi le scénario canonique) bypasse les atexit handlers, donc le compteur LLVM côté child n’est pas flushé. La ligne std::process::abort(); apparaît non couverte alors qu’elle s’exécute bel et bien (sinon l’enfant ne mourrait pas et le parent ne verrait pas WaitEvent::Killed). Compromis acceptable, hors de portée raisonnable : couvrir cette ligne demanderait un appel explicite à __llvm_profile_write_file() (lien runtime LLVM, non portable hors instrumentation) ou un signal catchable + handler — complications non justifiées par le bénéfice.

Tests : 35 dans air-sys-types (dont les 9 hérités du bootstrap + 26 nouveaux pour Errno/Signal/Clone*/Wait*/PidFd) ; 26 dans air-sys-syscall (dont les 6 hérités pour getpid/gettid + 20 nouveaux pour clone3/waitid/siginfo_to_wait_event/build_kernel_clone_args/errno_from_negative_syscall_ret) ; 1 binaire d’intégration main_thread_invariant inchangé. Total 62 tests passants.

Couverture finale : 100 % lignes production sur les quatre fichiers source ; régions à 96 % global (gap concentré dans les expect() d’invariants par construction et les arms de match jamais pris, conformément à la politique consignée dans la première moitié de la session).

Session 2026-05-22 — Bootstrap couche 0 : workspace + getpid/gettid

Première session d’implémentation. Création de la branche feat/layer-0-bootstrap et mise en place du workspace Cargo (edition 2024, rust-toolchain.toml pinné à 1.85.0) avec les deux crates fondatrices air-sys-types et air-sys-syscall. Deux premières fonctions de la famille process implémentées strictement conformes à la spec (docs/specs/layer-0/family-process.md) : getpid() -> Pid et gettid() -> Tid, syscalls appelés directement via core::arch::asm! sur x86_64 et aarch64, sans passer par la libc. Tests unitaires et test d’intégration harness = false pour l’invariant gettid()==getpid() sur le thread principal (impossible à observer dans un #[test] standard du fait du test runner cargo).

Décision méthodologique consignée — seuil régions llvm-cov. Les régions llvm-cov < 100 % sont acceptées lorsqu’elles correspondent à des branches d’échec d’assertions de test (assert_eq!/assert_ne!) ou de .expect() sur des invariants garantis (par exemple, le retour POSIX strictement positif de getpid et gettid consommé par NonZeroI32::new(...).expect(...)), jamais prises en exécution normale. Le critère du Principe 1 (100 % lignes + branches) reste tenu et reste le seuil normatif ; le seuil « régions » n’est pas un objectif. Motivation : ces branches inatteignables existent par construction (preuve de l’invariant côté kernel/POSIX), les supprimer demanderait soit de dégrader le diagnostic (perdre les messages d’assertion qui aident l’humain en cas de régression sur le wrapper asm!), soit d’enfreindre le Principe 5 en remplaçant un expect() vérifié par un unsafe { new_unchecked } — choix qui transformerait une violation d’invariant bruyante en UB silencieux. À noter pour tout futur contributeur tenté de viser 100 % de régions : c’est un faux objectif.

Décision afférente — préférence stricte pour la construction vérifiée. Sur la couche 0, NonZeroI32::new(...).expect(...) est préféré à unsafe { NonZeroI32::new_unchecked(...) } même quand l’invariant POSIX garantit la non-nullité. Raisons : (1) Principe 5, pas d’optimisation avant mesure sur un chemin non chaud ; (2) si l’asm! des syscalls est jamais mal câblé (numéro de syscall ou registre), new_unchecked(0) est un UB silencieux alors qu’un expect() rend la violation bruyante et diagnostiquable ; (3) la branche d’échec, jamais prise, relève du même statut que les autres branches d’invariants — couverture lignes conservée à 100 %, voir décision ci-dessus.

Session 2026-05-20 (suite 5) — Rédaction du README final (EN + FR)

Refonte du README.md à la racine du dépôt. Le précédent était une version provisoire qui décrivait essentiellement la structure de la documentation. Le nouveau est la vitrine publique du projet prête pour le jour de l’ouverture publique (avec un bandeau de statut à retirer ce jour-là).

Décision sur la langue : README principal en anglais (convention GitHub internationale, anticipation de l’ouverture publique vers une communauté internationale), avec un README-fr.md complémentaire qui fournit la version française. Les deux pointent l’un vers l’autre. Validation BDFL.

Contenu du nouveau README (les deux versions ont une structure identique) :

  • Bandeau de statut « phase de design, pas encore ouvert publiquement » à retirer le jour de l’ouverture.
  • Présentation d’Air en deux paragraphes : stack en couches, deux profils (air-base console, air-desktop graphique), Rust polyglotte via ABI C.
  • État du projet : phase de design, jalons documentaires atteints, pas de code public, pas de date publique.
  • Architecture en bref : les 6 couches en quelques lignes avec liens vers ADR-001 (AirCom), ADR-014 (machines de référence), ADR-025 (builds reproductibles).
  • Table de documentation avec chemins et langues, indiquant clairement ce qui est FR / EN / les deux.
  • Section « Building Air » : « Not yet applicable » + stratégie documentée (toolchain pinnée, Cargo.lock committé, reproductibilité bit-pour-bit). Pas de procédures fantômes.
  • Section « Contributing » : « Not yet open to external contributions », explication du rôle BDFL actuel, énumération de ce qui sera en place à l’ouverture publique (CONTRIBUTING.md, CODE_OF_CONDUCT.md, SECURITY.md, RFC, DCO).
  • Communication : domaine air-desktop.org (réservé, pas de site), org GitHub air-desktop-project, channels à annoncer plus tard.
  • Trademark : intention de dépôt à la fondation, modèle Linux Foundation pour la politique d’usage.
  • Licence : MPL 2.0, renvoi vers LICENSE.

Mises à jour de cohérence : JOURNAL.md mis à jour, nouveau prochain chantier identifié = CONTRIBUTING.md détaillé (le README y fait référence et la matière existe déjà dans docs/setup/phase-B*.md). Le backlog court terme est réordonné en conséquence.

Fichiers committés via Claude Code en local : README.md (modifié), README-fr.md (nouveau), docs/JOURNAL.md (modifié). Commit 048e2cb signé GPG + DCO, vérifié par GitHub (reason: valid), push b3737d6..048e2cb sur origin/main. Diff cumulé : +277 / −54 lignes.

Session 2026-05-20 (suite 4) — Mise en place du dépôt GitHub privé

Sujet soulevé par Thierry : « git c’est aussi un historique… une mémoire ». Création du repo pour versionner immédiatement la documentation, même si on ne produit pas encore de code, afin de capturer l’historique fin des décisions.

Décision stratégique tranchée par BDFL : repo privé d’abord, pas public. Le moment du flip vers public sera l’ouverture publique du projet, à maîtriser plus tard quand README final, CONTRIBUTING, SECURITY, marque protégée et channels communauté seront en place. Cohérent avec la politique d’édition design-phase actuelle.

Nouvelle pratique actée : pour les tâches d’exécution locale (git, gh, build, etc.), Claude produit des prompts autonomes pour Claude Code que Thierry copie-colle dans son terminal. Thierry n’intervient qu’en dernier recours. Mémoire sauvegardée pour les futures sessions.

Travaux effectués :

  • Côté Cowork (préparation) : LICENSE créé (texte officiel MPL 2.0), .gitignore créé (macOS/Linux/Windows/éditeurs/backups, avec note que Cargo.lock sera committé), README.md enrichi d’un bandeau « phase de design pré-ouverture publique » à retirer le jour J, git init -b main exécuté dans le workspace avec configuration locale user.name / user.email. Rédaction du prompt Claude Code détaillé (pré-vols, création repo, commit signé, push, protections de branche, rapport final).
  • Côté Claude Code (exécution locale) :
    • Repo air-desktop-project/air créé en privé sur GitHub.
    • Première authentification gh rafraîchie pour ajouter les scopes admin:gpg_key et admin:public_key.
    • Clé GPG A9F56C4D9F59EE03 ajoutée au compte GitHub tdelhaise.
    • Commit initial b3737d6 signé GPG + DCO Signed-off-by, vérifié par GitHub (verified: true, reason: valid).
    • Push origin main réussi.
    • Protections de branche non appliquées : le tier gratuit GitHub Free bloque l’API de protection sur les repos privés d’org. Documenté comme dette connue (cf. section ci-dessus).
  • Côté Cowork (clôture) : JOURNAL mis à jour avec l’état du dépôt, le SHA du commit initial, la dette « protections de branche », et la nouvelle pratique de délégation à Claude Code.

État final : dépôt opérationnel, signé, sourcé, prêt à recevoir tous les travaux ultérieurs. Premier point de bascule franchi.

Session 2026-05-20 (suite 3) — Enrichissement d’ADR-001 (AirCom vs D-Bus)

Sujet soulevé par Thierry : le choix de ne pas reposer sur D-Bus comme transport principal est l’un des plus visibles et contestables d’Air, mais l’ADR-001 dans sa forme initiale faisait 12 lignes — anormalement court par rapport aux autres ADRs phase 0 (145 à 210 lignes). Pour un projet qui prétend à la transparence et à la qualité argumentative, cette dette de documentation pouvait créer de la défiance chez les contributeurs venant de l’écosystème D-Bus traditionnel.

Approche retenue : enrichir substantiellement ADR-001 selon trois principes posés par BDFL — pragmatisme, sources d’informations, cohérence/incohérence avec les exigences de qualité d’Air. Travaux effectués :

  • Rédaction d’un ADR-001 substantiel (197 lignes, dans la fourchette des autres ADRs phase 0) avec dix sections : contexte (reconnaissance de la place de D-Bus dans l’écosystème), décision en 4 points fermes, justification technique en 5 sous-sections (broker centralisé, typage/versioning, FD passing, zero-copy/back-pressure, sécurité par policy XML — chaque propriété de D-Bus mise en regard d’une exigence Air nommée : Charte principe 4, Principe d’ingénierie 8, ADR-010, ADR-012, ADR-014, etc.), alternatives évaluées (kdbus 2013-2017, bus1 2016-2018, dbus-broker, évolution incrémentale), synthèse de AirCom, périmètre exact du pont D-Bus dans Air (sd-bus peer-to-peer pour systemd, air-dbus-bridge optionnel, xdg-desktop-portal-air comme exception assumée), articulation avec ADR-002/005/010/012/013/022, FAQ anticipée de 6 questions courantes, conséquences (bénéfices/coûts/risques), statut futur.
  • Sources publiques citées : spécification D-Bus officielle, dépôt dbus-broker (Tom Gundersen / David Herrmann), trois articles LWN sur kdbus et bus1. La pédagogie repose sur des faits vérifiables, pas sur des opinions. Note : les liens LWN précis sont à vérifier avant ouverture publique.
  • Inspirations AirCom assumées ouvertement : XPC (macOS), Cap’n Proto, FIDL (Fuchsia), Mojo (Chromium), Binder (Android). Cela coupe court à toute accusation de NIH non avouée.
  • Ton : factuel, sans posture évangéliste, reconnaissance explicite de la place de D-Bus dans l’écosystème, exception xdg-desktop-portal-air assumée, FAQ qui désamorce 6 questions classiques (« pourquoi pas étendre D-Bus ? », « est-ce que c’est du NIH ? », etc.).
  • Mises à jour de cohérence : registre passé en v2.3 (bloc historique enrichi, table inchangée — la décision n’a pas évolué, seule sa documentation l’a fait).

Relu et validé par BDFL sans modification.

Complément vision (priorité 2 du plan initial) : ajout d’un paragraphe pédagogique de 4-5 phrases dans vision-fr.md et vision-en.md, inséré juste après le paragraphe décrivant la couche 2. Le paragraphe (a) reconnaît la place de D-Bus dans l’écosystème Linux (systemd, NetworkManager, BlueZ, polkit, freedesktop), (b) cadre la décision « Air parle D-Bus pour interop, conçoit AirCom pour son cœur natif », (c) explicite les besoins spécifiques (sécurité capability-based, stabilité 10 ans, perf matériel modeste) que D-Bus ne couvre pas par construction, (d) renvoie vers ADR-001 pour le détail. Ton sobre, factuel, cohérent avec le reste de la vision. Le sujet est désormais traité à deux niveaux : visible et accessible dans la vision (porte d’entrée publique), argumenté et sourcé dans ADR-001 (référence technique).

Session 2026-05-20 (suite 2) — Passe 1 de nettoyage de la macro-architecture

Résolution de la dette de cohérence entre architecture/macro-architecture-fr.md (rédigée en early-stage) et les ADRs phase 0 (019-025) finalisés depuis. Travaux effectués :

  • Inventaire complet des divergences :
    • 35 références prospectives ADR-018 à ADR-026 désignant des services système couche 5 non instruits (protocole privé AirCom air-wm/apps, air-screencapture, air.system.appearance/theming, air-share, air-firewalld, air-power, air-printd/air-bluetoothd/air-nfcd, air-prefs, air-keychain), en collision avec les ADRs phase 0 réels.
    • Distinction subtile : ADR-022 prospectif avait deux significations dans le doc (framework networking couche 2 d’une part, air-firewalld couche 5 d’autre part) — neutralisations séparées.
    • Section 2 (couche 0) entièrement obsolète : crate air-sys unique avec 11 sous-modules incohérents avec le découpage actuel des specs, modèle d’erreur AirSysError enum riche contredisant ADR-019 (Errno minimaliste), conventions ADR-021 absentes, choix rustix non confirmé.
  • Révision de la section 2 (couche 0) — réécriture de la sous-section « Crate principale » et des sous-sous-sections détaillées :
    • Découpage en deux crates air-sys-types (types fondamentaux) + air-sys-syscall (wrappers de syscalls).
    • Liste des 10 familles actuellement spécifiées (process, fs, mem, signal, time, net, ipc, security, system, io_uring) avec pointeurs vers les specs.
    • Identification des 3 trous à combler ultérieurement : famille device, famille ebpf, confirmation rustix.
    • Module io_uring introduit avec référence à ADR-022 et ses 4 Temps.
    • Sous-section « Gestion d’erreur » alignée sur ADR-019 (Errno #[repr(transparent)] sur NonZeroI32, sans contexte ; enrichissement aux couches 1+).
    • Nouvelle sous-section « Conventions transverses » consignant les 5 conventions d’ADR-021.
    • Sous-section rustix clarifiée en « hypothèse de travail à confirmer en début d’implémentation, dans le cadre du workflow ADR-024 ».
  • Neutralisation par sed multi-passes des 35 références prospectives. Toutes remplacées par des marqueurs neutres « ADR à produire — service [X] » qui ne pré-réservent aucun numéro. Aucune occurrence prospective résiduelle (vérification par grep exhaustive).
  • Mises à jour de cohérence : INDEX.md passé en v1.3 (statut macro-architecture mis à jour, section « Point d’attention » réécrite, backlog réordonné), JOURNAL.md mis à jour (état courant, prochain chantier = README final, dettes connues purgées des dettes résolues et complétées des dettes restantes).

Vérifications finales : aucune référence ADR cassée, table des matières intacte (10 sections, 1764 lignes — quasi inchangé en volume), références aux ADRs réels uniquement (001-017 fondateurs, 018 imagerie, 019-022/024 phase 0).

Session 2026-05-20 (suite) — ADR-025 « Stratégie de builds reproductibles »

Clôture de la série de design phase 0. Travaux effectués :

  • Trois arbitrages structurants validés par BDFL : (1) périmètre = artefacts Rust natifs uniquement pour la phase 0 (bundles .airapp, images OS = ADRs ultérieurs) ; (2) calendrier = bloquant en CI dès le premier commit phase 0 ; (3) outillage = approche minimaliste Rust natif + discipline, pas de Nix ni de système de build alternatif.
  • Rédaction d’ADR-025-builds-reproductibles-fr.md (~210 lignes) couvrant : contexte (rattachement à Charte principes 3 et 5, ADR-013, ADR-015, phase-C), décision en 4 points fermes, mécanismes techniques (toolchain pinnée via rust-toolchain.toml exact, Cargo.lock committé, container Debian par digest SHA-256, env vars whitelistées, SOURCE_DATE_EPOCH dérivée du commit, RUSTFLAGS avec --remap-path-prefix + codegen-units=1 + --build-id=none, linker mold, discipline sur build.rs et code généré), vérification CI multi-pipelines (pr-full double-build + diffoscope, pr-hardware cross-machine, main-nightly triple-build), articulations avec ADR-010/013/014/015/024 et Principes 1/6/7, conséquences (bénéfices/coûts/risques), statut futur (ADRs ultérieurs pour bundles et image OS).
  • Mises à jour de cohérence : registre passé en v2.2 (ajout ADR-025, retrait de la mention « à produire »), INDEX.md passé en v1.2 (statut ADR-025 = OK, statistiques recalculées, ordre de priorité court terme revu), JOURNAL.md mis à jour (état courant, prochain chantier = révision macro-architecture passe 1).

État final : zone de design phase 0 close. Prochain chantier identifié = révision macro-architecture passe 1.

Session 2026-05-20 — Réorganisation documentation et résolution doublon ADR-018

Cohérence et structuration. Travaux effectués :

  • Réorganisation physique : déplacement des 22 fichiers traînant à la racine vers docs/ selon l’arborescence cible décrite dans INDEX.md (vision/, charte/, principes-ingenierie/, adrs/, architecture/, specs/layer-0/). Suppression des obsolètes (ADR-009-bis, ancien INDEX.md racine, .DS_Store).
  • Résolution du doublon ADR-018 : la série phase 0 instruite (018-erreurs, 019-signaux, 020-conventions, 021-io_uring, 022-runtime, 023-deps, 024-reproducible prévu) a été décalée de +1 pour libérer ADR-018 au profit du « Modèle d’imagerie » fondateur acté en registre v2.0. Nouvelle numérotation : 019=erreurs, 020=signaux, 021=conventions, 022=io_uring, 023=runtime, 024=workflow-deps, 025=reproductibles (à produire).
  • Convention de nommage unifiée : tous les ADRs suivent désormais ADR-NNN-titre-fr.md. Les 6 ADRs phase 0 ont été renommés depuis le style anglais sans préfixe.
  • Propagation des renumérotations : titres internes H1 des 6 ADRs phase 0 mis à jour ; références croisées corrigées dans setup/ et specs/layer-0/.
  • Registre des ADRs : passé en v2.1 (intégration des ADRs phase 0 + bloc historique). Liens vers les fondateurs apparentés corrigés pour utiliser les chemins relatifs ../vision/, ../charte/, etc.
  • Index : docs/INDEX.md passé en v1.1 (arborescence à jour, point d’attention sur la collision macro-architecture, statistiques recalculées).
  • README.md racine : refondu pour décrire la nouvelle structure et pointer vers docs/INDEX.md.
  • Identification de la dette macro-architecture vs ADRs phase 0 (cf. section « Dettes connues »).

État final vérifié : 50 fichiers .md dans docs/, 25 ADRs sous docs/adrs/, aucun lien markdown cassé, aucune référence aux anciens chemins.

Session 2026-05-18 — Documents fondateurs version 1.0

Finalisation et figement des documents fondateurs en v1.0 :

  • Vision (FR + EN), Charte (FR + EN), Principes d’ingénierie (FR + EN).
  • Registre des ADRs v2.0 (restructuré en fichiers individuels par ADR).
  • 17 ADRs fondateurs (001 à 017) finalisés.
  • ADR-018 « Modèle d’imagerie » instruit en phase de design pré-ouverture publique.
  • ADR-009 révisé pour intégrer l’amendement « API exclusivement déclarative » (anciennement envisagé sous nom ADR-009-bis ; politique d’édition design-phase appliquée — pas de pattern -bis avant ouverture publique).
  • Politique d’édition design-phase actée : avant ouverture publique, les amendements aux documents fondateurs s’intègrent directement, pas par fichier -bis.

Sessions antérieures à 2026-05-18 — Spécifications phase 0

Sessions consacrées à la spécification de la couche 0 et du runtime async. Productions :

  • 6 ADRs phase 0 (numérotés à l’époque 018 à 023, renumérotés 019 à 024 le 2026-05-20).
  • 9 specs de familles de syscalls dans specs/layer-0/ (process, fs, mem, signal, time, net, ipc, security, system).
  • Spec partielle air-sys-types (~25 types).
  • Spec io-uring-overview (synthèse, détail Temps 1-4 à produire).
  • Documents de setup phases A (décisions techniques), B1 (structure workspace), B2 (conventions opérationnelles), C (infrastructure), D (communauté).
  • Document de macro-architecture des 6 couches (rédigé en early-stage, dette de cohérence à traiter — cf. section « Dettes connues »).

Licence du document : MPL 2.0 Statut : journal de bord interne, mis à jour à chaque fin de session.