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

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)

#SujetDécisionBase
0.1Nommage des cratesair-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.2Wire formatCap’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.3Control planeUnix 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.4Data planememfd_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.5CapabilityUn 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.6Surfaces APIDeux 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.7Pub-subDiffusion 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.8Registry / bootstrapUn 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éfixe aircom redouble le mot « Air » (AirCom → air-aircom). Cette spec acte le jeu air-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-uring via air-async, air-shm) — jamais la couche 0 en direct (check-layers). air-registry vit en couche 5. Cette spec couvre les crates couche 2 et le contrat que air-registry doit honorer ; l’implémentation de air-registry relè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

CrateCoucheno_stdRôleDépendances clés
air-com-proto2✅ (+alloc)Cœur sans-IO : les 9 composants ADR-091. Fuzzable en isolation.air-base-core, capnp (à terme)
air-com2❌ (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-schema2Schémas .capnp d’AirCom + code Rust généré commité.capnp, air-com-codegen (build)
air-com-codegen2 (outil)Enveloppe capnpc : regenerate.sh, vérification de compat ABI (ADR-012). Hors chemin de build critique (ADR-025).capnpc
air-registry5Dé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 = un recvmsg. Le Framer est trivial (identité + garde MAX_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 longueur u32 borné. 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 fds SCM_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 par air-com-codegen (règles de compat capnp).

3.3 (3) StateMachine

  • États de connexion : OpeningEstablishedDrainingClosed, plus Faulted (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_RIGHTS hors-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 Faulted sans 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 :UInt32 par 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 un SO_RCVTIMEO/poll calculé. 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, via AirUnixSeqpacket / AirUnixSeqpacketListener (couche 1, couche-1-v2.1). Frontières de message préservées ⇒ Framer trivial ; FD passing natif (SCM_RIGHTS).
  • Adresse : air-registry distribue des adresses de socket (namespace abstrait de préférence — pas de fichier à nettoyer — via AirUnixAddress::from_abstract).
  • Pilote async : air-com câble le cœur à air-asyncaccept/recv/send multishot, arm_recvmsg/arm_sendmsg pour le FD passing async (livrés #333).
  • Pilote sync : air-com câble le cœur à air-socket synchrone (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_WRITE pour un partage read-only ; F_SEAL_SHRINK|GROW systématiques) puis passe le fd en control plane via SCM_RIGHTS.
  • Message control plane : ne porte qu’un ShmRef (slot + offset + length + sealed). Le récepteur mmap le 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 de F_SEAL_WRITE, ownership exclusif documenté.
    • Taille : multiple de page ; length logique ≤ taille du segment (validé en amont).
  • Vues possédées : AirShmMap (&[u8]) / AirShmMapMut (&mut [u8]) — pas d’unsafe exposé.

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 le CapRef du 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 un CapRef + le fd correspondant dans le même recvmsg.
  • Révocation : fermeture du fd côté serveur ⇒ le pair reçoit EOF/ECONNRESET, la StateMachine transite Closed.
  • 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-shm scellé), 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 avance writeSeq (release) ; chaque consommateur garde son readSeq local.
  • 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 de generation/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::event d’amorçage) ; chaque abonné mmap en read-only (l’anneau est scellé F_SEAL_WRITE cô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é par air-async (async) ou poll (sync).
  • Distinct du RPC : le pub-sub ne passe pas par la StateMachine requête/réponse ; c’est un canal séparé (channelId dédié), spécifié comme un 9ᵉ patron autonome. Le cœur expose ring_publish(&[u8]) -> Result (producteur) et ring_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_blocking partagent le même air-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-com C-ABI (zone air-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-launchd passe à chaque service son fd de capability initial par SCM_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-registry vérifie l’entitlement du demandeur puis renvoie un fd de connexion vers le service (une nouvelle SEQPACKET connecté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-registry ne 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.2 MessageKind::call) désigne { objectId, methodId, args capnp } ; le return/error porte le résultat. Le channelId (#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œur air-com-proto n’expose aucune fn unsafe. Les rares unsafe du pilote (conversion de fd bootstrap, mmap via air-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/machete verts ; 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.ContenuSortie
0Fondation (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
1Schéma capnp v1 (air-com-schema, §3.2) + Codec réel (remplace le placeholder) + versioning CI (air-com-codegen). Fuzz codec.schéma figé
2StateMachine multi-états + Multiplexer (channelId) + Handshaker (négo de version). Model-based tests.RPC concurrent
3Transport SEQPACKET de bout en bout (pilote async et sync) + test croisé sync↔async. FD passing async en régime établi.2 surfaces
4Data plane zero-copy en régime établi (§5) + Flow Controller (crédits).streaming borné
5Pub-sub anneau (§7) : ring_publish/ring_poll, perte détectée, réveil eventfd/futex. Fuzz ring.diffusion 1→N
6Contrat air-registry matérialisé (mock couche 2) : lookup/register par fd. (Impl couche 5 = spec dédiée.)découverte
7Inté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 (zone air-stable, callbacks façon air-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.