Spec couche 2 — AirCom (IPC natif d’Air)
Spécification technique — Version 1.0 (décisions validées BDFL 2026-07-12). Couche 2 « Modèle d’objet, IPC, services fondamentaux ».
Autorité et périmètre. Cette spec fige les décisions laissées ouvertes par le rapport de design AirCom (§7) et par ADR-001 (« Statut futur » : le wire format exact, le format du registre… feront l’objet d’ADRs et de specs). ADR-001 reste immuable dans son principe (AirCom = transport interne, D-Bus pour l’interop) ; cette spec en est la déclinaison technique contraignante.
Ce qu’elle normalise : le jeu de crates et leurs couches, le wire format, le transport (control plane + data plane), le modèle de capability, l’anatomie sans-IO à 9 composants (ADR-091), les deux surfaces (synchrone et asynchrone) de première classe, le pub-sub par anneau de mémoire partagée, et le bootstrap
air-registry. Les incréments d’implémentation figurent en §12.
Section 0 — Décisions structurantes (arbitrées BDFL)
| # | Sujet | Décision | Base |
|---|---|---|---|
| 0.1 | Nommage des crates | air-com (pilote) · air-com-proto (cœur sans-IO) · air-com-schema (schémas .capnp) · air-com-codegen (compilateur/outil) · air-registry (découverte, couche 5). La macro-architecture §B (air-aircom*) est réconciliée sur ce jeu (note ci-dessous). | ADR-029, design §7.1 |
| 0.2 | Wire format | Cap’n Proto (capnp = "=0.26.0", pur Rust, zéro C), déjà audité/mutualisé. Codegen commité (regenerate.sh), hors chemin de build critique. | ADR-040, ADR-001, ADR-025 |
| 0.3 | Control plane | Unix SOCK_SEQPACKET (frontières préservées + FD passing). Wrapper AirUnixSeqpacket livré (couche 1, couche-1-v2.1). | design §4.3, macro-arch §B |
| 0.4 | Data plane | memfd_create + mmap SHARED + F_ADD_SEALS, fd partagé par SCM_RIGHTS. Façade air-shm livrée (couche 1). Zero-copy. | design §4.3/§6.5 |
| 0.5 | Capability | Un fd vers une connexion AirCom = la capability (+ métadonnées de type). Unforgeable par le noyau, pas de token/crypto. Révocation = fermeture fd ; délégation = re-SCM_RIGHTS. | ADR-010, ADR-001 §117 |
| 0.6 | Surfaces API | Deux surfaces de première classe : synchrone (sur air-socket) et asynchrone (sur air-async/io_uring). Le cœur sans-IO les sert toutes deux sans duplication de logique protocolaire. | design §7.7, ADR-038 |
| 0.7 | Pub-sub | Diffusion 1→N par anneau en mémoire partagée (memfd scellé, un producteur / N lecteurs, zero-copy), distinct du RPC requête/réponse p2p. | suivi, ADR-001 (anti-broker) |
| 0.8 | Registry / bootstrap | Un fd par service : air-launchd (couche 5) distribue à chaque service son fd de capability initial par SCM_RIGHTS selon les entitlements signés ; découverte ultérieure par lookup nommé auprès de air-registry (capability implicite universelle) → renvoie un fd de connexion. Pas d’autorité ambiante. | ADR-010, macro-arch §B |
Note de réconciliation (macro-architecture §B). Le doc d’architecture nomme
air-aircom/air-aircom-schema/air-aircom-codegen/air-registry. Le préfixeaircomredouble le mot « Air » (AirCom → air-aircom). Cette spec acte le jeuair-com*— déjà bâti et re-scellé (couche-1-v2.1, PR #333) — plus court et sans redondance. La macro-architecture sera alignée par édition documentaire (grep, aucune référence cassée), pas par RFC (aucune décision de fond ne change ; ADR-029 tranche le style de nom).
Section 1 — Position et méthode
AirCom est l’IPC natif d’Air : le transport par lequel les services et les
applications .airapp s’invoquent, sans broker central (le refus de D-Bus est
argumenté par ADR-001). Il se déploie sur le motif sans-IO normatif d’Air
(ADR-091) : un cœur pur
(fuzzable, sans socket ni horloge) et un pilote mince qui le câble à l’I/O.
┌─────────────────── air-com-proto (couche 2, no_std+alloc) ───────────────┐
octets ───▶ │ Framer → Codec → StateMachine → (Session Context) │ ───▶ messages typés
(feed) │ CŒUR SANS-IO — pur, FUZZABLE, zéro socket / async / horloge │ + octets (poll_transmit)
└───────────────────────────────────────────────────────────────────────────┘
▲ API sans I/O (feed / poll_transmit / poll_timeout)
┌───────────────────┴─────────── pilotes (couche 2) ───────────────────────┐
│ air-com (async, sur air-async/io_uring) · air-com (sync, sur air-socket) │
│ control plane SEQPACKET + data plane memfd (air-shm) + SCM_RIGHTS │
└───────────────────────────────────────────────────────────────────────────┘
Rappel de couches. Le cœur (
air-com-proto) et les pilotes (air-com) sont couche 2. Ils consomment des briques couche 1 (air-socket,air-uringviaair-async,air-shm) — jamais la couche 0 en direct (check-layers).air-registryvit en couche 5. Cette spec couvre les crates couche 2 et le contrat queair-registrydoit honorer ; l’implémentation deair-registryrelève d’une spec couche 5 ultérieure.
1.1 Le cœur ne détient aucun fd (invariant capital)
La capability est un fd ; le cœur sans-IO ne détient aucun fd. Le Session Context (#8) porte les métadonnées de capability (le quoi : type, droits négociés, ids) ; le pilote détient les fds (le comment). Cet invariant est ce qui rend le cœur pur et fuzzable, et ce qui autorise une seule logique protocolaire à servir les deux surfaces (sync et async) — elles ne diffèrent que par le pilote.
Section 2 — Jeu de crates
| Crate | Couche | no_std | Rôle | Dépendances clés |
|---|---|---|---|---|
air-com-proto | 2 | ✅ (+alloc) | Cœur sans-IO : les 9 composants ADR-091. Fuzzable en isolation. | air-base-core, capnp (à terme) |
air-com | 2 | ❌ (std) | Pilotes I/O async (air-async) et sync (air-socket). Control plane + data plane + SCM_RIGHTS. | air-com-proto, air-async, air-socket, air-shm, air-sys-types |
air-com-schema | 2 | ✅ | Schémas .capnp d’AirCom + code Rust généré commité. | capnp, air-com-codegen (build) |
air-com-codegen | 2 (outil) | ❌ | Enveloppe capnpc : regenerate.sh, vérification de compat ABI (ADR-012). Hors chemin de build critique (ADR-025). | capnpc |
air-registry | 5 | ❌ | Découverte nommée + distribution de capability initiale. Contrat ici, impl en spec couche 5. | air-com, entitlements ADR-010 |
Pourquoi deux crates cœur/pilote (et non un seul proto/+io/) : air-async
est std (Rc/RefCell/thread-locals) ; un crate unique forcerait le cœur à
hériter de std et le ferait entrer dans le build de fuzz. La séparation garantit un
cœur no_std réellement isolé (fuzz sans socket ni exécuteur). Précédent
quinn-proto/quinn.
Section 3 — Anatomie sans-IO à 9 composants (spec du cœur)
Les 9 cases d’ADR-091 §2, spécifiées pour AirCom. Le squelette #333 a livré 1/2/3/8 et marqué absents 4/5/6/7/9 ; cette spec en définit la cible.
3.1 (1) Framer
- Contrôle : sur
SEQPACKET, le noyau délimite — une trame = unrecvmsg. Le Framer est trivial (identité + gardeMAX_CONTROL_FRAME_LEN). - Données : le data plane ne passe pas par le Framer (voir §5) — seule la référence (id/offset/len de segment) transite en control plane.
- Compat descendante : sur un transport
STREAM(bootstrap, ou pilote sync sans SEQPACKET), le Framer applique un préfixe de longueuru32borné. Les deux Framers exposent la même interface au Codec.
3.2 (2) Codec — Cap’n Proto
- (dé)sérialise message typé ↔ octets via
air-com-schema. Pur, sans I/O : vit dans le cœur, fuzzable octet-par-octet. - Premier schéma (
air-com-schema/aircom.capnp, figé par cette spec en v1) :struct Envelope:requestId :UInt64,channelId :UInt32,kind :MessageKind(enum :call,return,error,event,credit,close),payload :Data,capRefs :List(CapRef).struct CapRef:slot :UInt16(indice dans le tableau de fdsSCM_RIGHTS),type :UInt32(métadonnées de type de capability). Aucun fd dans le message — seulement l’indice de slot ; le pilote apparie slot↔fd reçu.struct ShmRef(data plane) :slot :UInt16,offset :UInt64,length :UInt64,sealed :Bool.
- Versioning (ADR-012) : champs réservés jamais réutilisés ; évolution par
ajout de champs optionnels ; interfaces incompatibles = nouveau type coexistant
(
EnvelopeV2), vérifié en CI parair-com-codegen(règles de compat capnp).
3.3 (3) StateMachine
- États de connexion :
Opening→Established→Draining→Closed, plusFaulted(violation protocolaire). Transitions illégales rejetées (testé). - Par requête :
Idle → AwaitingReturn → {Returned | Errored | Cancelled}. Le Multiplexer (#6) autorise plusieurs requêtes concurrentes par connexion. - Aucune I/O, aucune horloge : les échéances arrivent par
poll_timeout(voir #7), la StateMachine ne fait qu’appliquer les transitions.
3.4 (4) Handshaker
- La capability est déjà établie (par
SCM_RIGHTShors-bande, §6). Le Handshaker ne négocie pas l’identité — il négocie la version de schéma et le jeu d’extensions (#9) :Hello{ schemaVersion, extensions, maxChannels, maxFrame }échangé aux deux sens ;- intersection déterministe (min de version, extensions communes) ;
- échec → transition
Faultedsans I/O (le pilote ferme).
- Pur :
feed(hello_bytes) -> HandshakeOutcome,poll_transmit() -> Option<bytes>.
3.5 (5) Flow Controller
- Back-pressure explicite (reactive streams, exigence ADR-001) pour le streaming
data plane : crédits par canal (
MessageKind::credit), fenêtre bornée. - Le producteur ne publie une nouvelle référence shm que s’il a du crédit ; le consommateur rend du crédit après lecture. Jamais de file non bornée en mémoire.
- Pour le pub-sub anneau (§7), le back-pressure est structurel (l’anneau a une taille fixe ; un lecteur lent perd des entrées et l’apprend via un compteur de génération — sémantique overwrite, choisie pour les events best-effort).
3.6 (6) Multiplexer
channelId :UInt32par requête/stream sur un seul fd (façon canaux SSH / streams h2). Isolation inter-canaux : un canal fautif n’affecte pas les autres.- Allocation de
channelId: pair-impair par rôle (client pairs, serveur impairs) pour éviter les collisions sans négociation, comme h2 stream ids.
3.7 (7) Timer Manager
- Horloge injectée dans le cœur :
poll_timeout(now: Instant) -> Option<Instant>. Le cœur calcule les échéances (timeout de requête, keepalive) ; il ne lit jamais l’horloge lui-même. - Le pilote async branche la roue de timers d’
air-async; le pilote sync branche unSO_RCVTIMEO/pollcalculé. Virtual clock en test (déterminisme).
3.8 (8) Session Context
- État par connexion : rôle (client/serveur), ids, métadonnées de capabilities négociées, fenêtres de flow control, table de canaux.
- Sécurité : tout secret éventuel zeroïsé au drop ; zéro fuite inter-sessions (chaque connexion a son contexte, pas de global mutable partagé). Ne détient aucun fd (§1.1).
3.9 (9) Extension hooks
- Registre de hooks à interface étroite : points d’extension pour les entitlements (ADR-010) et les extensions de schéma négociées au Handshake. Deny-by-default : une extension inconnue est ignorée, jamais fatale (sauf si requise par le pair).
Marqueurs. Tant qu’un composant n’est pas implémenté, il reste marqué absent (marqueur unité +
// NO …), conformément à ADR-091 §2 règle 3 (« on retrouve toujours les mêmes cases »). Le squelette #333 marque déjà 4/5/6/7/9.
Section 4 — Transport control plane (SEQPACKET)
- Socket : Unix domain
SOCK_SEQPACKET, viaAirUnixSeqpacket/AirUnixSeqpacketListener(couche 1,couche-1-v2.1). Frontières de message préservées ⇒ Framer trivial ; FD passing natif (SCM_RIGHTS). - Adresse :
air-registrydistribue des adresses de socket (namespace abstrait de préférence — pas de fichier à nettoyer — viaAirUnixAddress::from_abstract). - Pilote async :
air-comcâble le cœur àair-async—accept/recv/sendmultishot,arm_recvmsg/arm_sendmsgpour le FD passing async (livrés #333). - Pilote sync :
air-comcâble le cœur àair-socketsynchrone (AirUnixSeqpacket::{send,recv,send_fd,recv_fd}). Même cœur, même StateMachine.
Section 5 — Data plane (memfd zero-copy)
- Segment :
air-shm::AirShmSegment(couche 1) —create → ftruncate → mmap SHARED → add_seals. Le producteur scelle (F_SEAL_WRITEpour un partage read-only ;F_SEAL_SHRINK|GROWsystématiques) puis passe le fd en control plane viaSCM_RIGHTS. - Message control plane : ne porte qu’un
ShmRef(slot + offset + length + sealed). Le récepteurmmaple fd reçu et lit zero-copy — la charge ne transite jamais par le socket. - Conventions figées (v1) :
- Sealing obligatoire avant partage (un segment non scellé est refusé côté
réception →
Faulted). - Partage read-only ⇒
F_SEAL_WRITE+mmap PROT_READ. Partage read-write (rare, buffers négociés) ⇒ pas deF_SEAL_WRITE, ownership exclusif documenté. - Taille : multiple de page ;
lengthlogique ≤ taille du segment (validé en amont).
- Sealing obligatoire avant partage (un segment non scellé est refusé côté
réception →
- Vues possédées :
AirShmMap(&[u8]) /AirShmMapMut(&mut [u8]) — pas d’unsafeexposé.
Section 6 — Modèle de capability (fd + SCM_RIGHTS)
- Une capability = un fd vers une connexion AirCom (
SEQPACKET), plus des métadonnées de type (portées par leCapRefdu message et le Session Context). Unforgeabilité par le noyau : on ne fabrique pas un fd qu’on n’a pas reçu. Pas de token, pas de nonce, pas de crypto pour la capability elle-même. - Distribution initiale :
air-launchd(couche 5) passe à chaque service son fd selon les entitlements signés (ADR-010) — un fd par service (§0.8). - Délégation : p2p, par re-
SCM_RIGHTSà travers AirCom lui-même (modèle seL4 / Mach ports). Un pair transmet unCapRef+ le fd correspondant dans le mêmerecvmsg. - Révocation : fermeture du fd côté serveur ⇒ le pair reçoit
EOF/ECONNRESET, la StateMachine transiteClosed. - Double verrou (renfort) : la sandbox io_uring couche 0
(
RestrictionSet::from_entitlements) traduit à terme les entitlements signés en restrictions — point d’intégration couche 5, non requis pour la v1 du transport.
Contrat cœur : le cœur ne voit qu’un indice de slot (
CapRef.slot) ; le pilote apparie slot↔fd. Le cœur reste sans fd, donc fuzzable.
Section 7 — Pub-sub : anneau de mémoire partagée (diffusion 1→N)
Au-delà du RPC requête/réponse p2p, AirCom offre une diffusion 1→N zero-copy pour
les notifications/events (ex. air-notifyd, premier service AirCom d’ADR-001).
- Mécanisme : un anneau en mémoire partagée (
air-shmscellé), un producteur / N consommateurs. Le producteur écrit une fois ; N abonnés lisent zero-copy. Cohérent avec le data plane (§5) et la raison d’être anti-broker d’ADR-001 (aucune copie par un intermédiaire). - Structure (figée v1) : en-tête
{ capacity, slotSize, writeSeq (atomique), generation }+ tableau de slots. Le producteur avancewriteSeq(release) ; chaque consommateur garde sonreadSeqlocal. - Sémantique de retard (overwrite / best-effort) : si un consommateur prend du
retard au-delà de
capacity, le producteur écrase les entrées les plus anciennes. Le consommateur détecte la perte (saut degeneration/writeSeq) et la signale (compteur de messages perdus). C’est le back-pressure structurel du §3.5 — choisi car les events sont best-effort (un abonné lent ne bloque pas le producteur ni les autres abonnés). - Établissement : le producteur publie le fd de l’anneau sur le control plane
(
ShmRef+MessageKind::eventd’amorçage) ; chaque abonnémmapen read-only (l’anneau est scelléF_SEAL_WRITEcôté producteur seulement — les abonnés ne peuvent pas corrompre l’anneau). - Réveil : un
eventfd(ou un futex sur mot d’en-tête) notifie les abonnés qu’une nouvelle entrée est disponible, piloté parair-async(async) oupoll(sync). - Distinct du RPC : le pub-sub ne passe pas par la StateMachine
requête/réponse ; c’est un canal séparé (
channelIddédié), spécifié comme un 9ᵉ patron autonome. Le cœur exposering_publish(&[u8]) -> Result(producteur) etring_poll() -> Option<(&[u8], lost: u64)>(consommateur) — purs, l’anneau concret (mmap) étant fourni par le pilote.
Section 8 — Surfaces API (synchrone et asynchrone)
Décision §0.6 : les deux surfaces sont de première classe. Une seule logique
protocolaire (le cœur air-com-proto) ; deux pilotes dans air-com.
8.1 Surface asynchrone (air-async, io_uring) — usage natif Rust
#![allow(unused)]
fn main() {
// Client
let conn = air_com::Connection::connect_async(cap_fd).await?; // cap_fd reçu par SCM_RIGHTS
let reply = conn.call(channel, method, &args).await?; // RPC multiplexé
// Serveur
let mut listener = air_com::Listener::from_fd_async(server_fd)?;
while let Some(conn) = listener.accept().await? { runtime.spawn(serve(conn)); }
// Pub-sub
let mut sub = air_com::Subscriber::attach_async(ring_fd)?;
while let Some((event, lost)) = sub.next().await { /* lost = messages ratés */ }
}
8.2 Surface synchrone (air-socket) — usage bloquant / scripts / bootstrap
#![allow(unused)]
fn main() {
let conn = air_com::Connection::connect_sync(cap_fd)?; // même Connection, pilote sync
let reply = conn.call_blocking(channel, method, &args)?;
}
- Contrat de non-duplication :
call/call_blocking,accept/accept_blockingpartagent le mêmeair-com-proto(Framer/Codec/State/Session). Un test de conformité croisé (client sync ↔ serveur async, et inversement) doit passer (§11) — c’est la preuve que le cœur est réellement partagé. - Façade C-ABI : une future
air-comC-ABI (zoneair-stable, ABI 10 ans, ADR-012) exposera le modèle à callbacks (cf.air-event) pour les consommateurs polyglottes. Hors périmètre v1 (Rust d’abord) ; réservé.
Section 9 — Bootstrap & découverte (air-registry)
Contrat que air-registry (couche 5) doit honorer ; l’implémentation relève
d’une spec couche 5.
- Un fd par service (§0.8) : à l’activation,
air-launchdpasse à chaque service son fd de capability initial parSCM_RIGHTS, selon les entitlements signés. - Capability implicite universelle : tout processus Air reçoit une capability
vers
air-registry(la seule autorité ambiante tolérée, car strictement de découverte). Format du handshake de découverte :lookup(serviceName) -> Result<fd, NotFound|Denied>:air-registryvérifie l’entitlement du demandeur puis renvoie un fd de connexion vers le service (une nouvelleSEQPACKETconnectée, ou une erreur). Pas d’adresse en clair à reconnecter soi-même par défaut — le registry remet le fd (unforgeable).register(serviceName, listenerFd): un service s’enregistre en cédant un fd d’écoute (ou un canal d’acceptation). Réservé aux services entitled.
- Pas de broker de données :
air-registryne relaie aucune charge applicative — uniquement la mise en relation initiale. Toute communication ultérieure est p2p (ADR-001 §« pas de broker central »).
Section 10 — Intégration au modèle d’objet (ADR-002)
AirCom transporte des invocations sur des AirObject (modèle d’objet C-ABI,
air-object).
- Une
call(§3.2MessageKind::call) désigne{ objectId, methodId, args capnp }; lereturn/errorporte le résultat. LechannelId(#6) isole les invocations concurrentes sur un même objet distant. - Le
cx.capability::<T>()de la macro-architecture §B se résout côté cœur en une entrée de Session Context (métadonnées de type) + côté pilote en un fd. - Bindings polyglottes : via le modèle d’objet C-ABI d’ADR-002 (la façade C-ABI réservée au §8.2), pas par génération de glue par classe.
Section 11 — Conformité & tests (couche 2, > 90 % + rigueur cœur)
- Cœur
air-com-proto— traité au standard couche fondatrice (proche 100 %) : tests unitaires + property-based (transitions StateMachine, round-trip Codec) + fuzz obligatoire par parseur externe :fuzz/framer(délimitation,MAX_*_LEN),fuzz/codec(capnp Envelope/CapRef/ ShmRef),fuzz/handshake(Hello malformé),fuzz/ring(en-tête d’anneau corrompu).- Model-based testing de la StateMachine (transitions illégales rejetées, pas de deadlock d’état). Virtual clock pour le Timer (déterminisme).
- Pilote
air-com— couverture > 90 % (cible couche 2) : round-trips end-to-end, FD passing async & sync, data plane shm zero-copy, pub-sub anneau (dont perte détectée par un abonné lent), fermeture/révocation. - Test croisé sync↔async (§8) : client sync ↔ serveur async et l’inverse — garantit le partage effectif du cœur.
unsafe: le cœurair-com-proton’expose aucune fnunsafe. Les raresunsafedu pilote (conversion de fd bootstrap, mmap viaair-shm) portent un// SAFETY:et restent localisés (vérifiécheck-safety-comments).check-layers: arêtes 2→2 et 2→1 uniquement ; jamais 2→0.- Barrière :
fmt/clippy -D/test/macheteverts ; couverture selon seuils ; Cap’n Proto = zéro nouvelle exception (mutualisée ADR-040,docs/EXCEPTIONS.md).
Section 12 — Incréments d’implémentation
| Inc. | Contenu | Sortie |
|---|---|---|
| 0 | ✅ Fondation (PR #333) — squelette 2 crates, composants 1/2/3/8, preuve Ping→Pong, 4 lacunes couche 1 comblées (air-shm, SCM_RIGHTS async, SEQPACKET, pont OwnedFd), preuve d’intégration zero-copy. | couche-1-v2.1 |
| 1 | Schéma capnp v1 (air-com-schema, §3.2) + Codec réel (remplace le placeholder) + versioning CI (air-com-codegen). Fuzz codec. | schéma figé |
| 2 | StateMachine multi-états + Multiplexer (channelId) + Handshaker (négo de version). Model-based tests. | RPC concurrent |
| 3 | Transport SEQPACKET de bout en bout (pilote async et sync) + test croisé sync↔async. FD passing async en régime établi. | 2 surfaces |
| 4 | Data plane zero-copy en régime établi (§5) + Flow Controller (crédits). | streaming borné |
| 5 | Pub-sub anneau (§7) : ring_publish/ring_poll, perte détectée, réveil eventfd/futex. Fuzz ring. | diffusion 1→N |
| 6 | Contrat air-registry matérialisé (mock couche 2) : lookup/register par fd. (Impl couche 5 = spec dédiée.) | découverte |
| 7 | Intégration modèle d’objet (ADR-002) : call sur AirObject. Premier service réel : air-notifyd (ADR-001). | 1er service |
Cible aval. AirCom est un prérequis de
air-sshd(la vraie cible produit, ADR-074) et du framework réseau. Le RPC (inc. 1–3) suffit àair-sshd; data plane et pub-sub (inc. 4–5) servent les services desktop (notifications, media, buffers GPU).
Section 13 — Questions résiduelles (hors périmètre v1, réservées)
- Façade C-ABI
air-com(zoneair-stable, callbacks façonair-event) pour consommateurs polyglottes — réservée, Rust d’abord. air-dbus-bridge(ADR-001 §interop) : exposer les services D-Bus (NetworkManager, BlueZ…) en AirCom — spec séparée.- Sandbox io_uring pilotée par entitlements (§6 double verrou) — intégration couche 5.
- Inspecteur/debugger de tronc AirCom (ADR-001 §outillage) — outil, spec séparée.
Licence du document : MPL 2.0.