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

ADR-136 — Descellement additif couche-1-v3.6 : air-json, le codec JSON possédé entre en couche 1

Statut : Accepté (2026-07-29). RFC de structure (ADR-015). Applique le modèle de re-sceau du sceau couche 1 (ADR-062) sous la délégation additive du superviseur (ADR-065). Met en œuvre ADR-114 (vague 2 #2), dont il est l’incrément d’exécution.

Catégorie : Descellement ADDITIF de la couche 1 scellée (couche-1-v3.5couche-1-v3.6). Aucun retrait, aucun changement de signature, aucune modification de comportement d’une primitive scellée : toute surface antérieure reste valide telle quelle. Ce qui est ajouté est une crate entière et neuveair-json — dont personne ne dépendait hier.

Le tag n’est pas posé par cet ADR. Conformément au modèle de re-sceau, la pose du tag couche-1-v3.6 revient au superviseur, après revue.

Contexte

ADR-114 a tranché : le codec JSON maison né dans air-sshd/src/json.rs (ADR-099 E.6) est promu en crate couche 1 partagée, parce que la CLI air-keystore (ADR-115, vague 2 #3) exposera un mode JSON agent-safe et a besoin du même codec. Rien, dans cette décision, n’exigeait de toucher à quoi que ce soit de scellé — mais une crate couche 1 de plus, c’est une surface couche 1 de plus, et le sceau porte sur la surface, pas sur les fichiers. D’où le présent ADR : il n’y a rien à desceller, il y a quelque chose à inscrire.

Décision

Descellement additif couche-1-v3.6, sur une seule crate — et cette crate est neuve.

1. La crate air-json (couche 1, no_std + alloc, zéro I/O)

Surface publique, entièrement nouvelle :

  • AirJsonValue — le modèle de valeur : Null, Bool(bool), Number(i64), String(String), Array(Vec<AirJsonValue>), Object(Vec<(String, AirJsonValue)>). Les noms de variantes sont ceux de la grammaire RFC 8259 — une autorité que le développeur peut consulter (ADR-029, zone 2).
  • AirJsonValue::get / as_str / as_i64 / as_bool / as_array — les accesseurs, qui rendent None hors de leur variante plutôt que de paniquer.
  • AirJsonValue::write_to / to_json_string — le sérialiseur (forme compacte).
  • parse(&[u8]) -> AirResult<AirJsonValue> — le parseur.
  • MAX_DEPTH — la borne d’imbrication, publique parce qu’elle fait partie du contrat : un appelant qui génère du JSON imbriqué a le droit de savoir jusqu’où il peut aller.

L’unique dépendance est air-base-core (arête 1→1). air-json n’appelle aucun syscall : elle ne touche ni air-sys-syscall, ni air-filesystem, ni rien qui ouvre un descripteur. C’est un pur codec, et c’est ce qui la rend consommable sans précaution par la couche 1 comme par les couches au-dessus.

2. Erreurs : AirError, pas un enum maison

Le JsonError d’origine (six variantes Copy) est remplacé par la monnaie d’erreurs d’Air (ADR-019) : parse rend AirResult<AirJsonValue>, et toute faute est un AirErrorKind::InvalidData — la catégorie « données mal formées », qui est exactement ce dont il s’agit. Ce qui distingue les cas vit dans le message, qui nomme la faute et localise l’octet (octet inattendu à la position 42, nombre invalide à la position 7…).

Le raisonnement : un appelant de couche 2 — CLI, démon — mélange déjà dans le même Result des erreurs d’I/O, de configuration et de parsing. Lui rendre un enum de plus l’obligerait à écrire une conversion à chaque frontière pour, au bout du compte, ne matcher que « c’est mal formé ». La position, elle, ne se perd pas : elle est dans le message, là où elle sert — à pointer la faute à un humain ou à un agent.

3. air-sshd cesse d’héberger le module

crates/air-sshd/src/json.rs et ses tests sont supprimés ; air-sshd dépend d’air-json (arête 2→1) et ses deux binaires (air-sshd config export|import, air-ssh config export|import) consomment la crate. La non-régression est portée par les tests d’interchange E.6 existants (tests/config_json.rs, tests/config_client_json.rs), inchangés et verts.

4. Durcissement exigé par le statut de crate publique

Le codec héritait déjà de « borné + jamais-panique + fuzzé ». Le passage en couche 1 lui impose la barre complète (ADR-114 §3) :

  • couverture 100 % lignes et branches, per-crate. Obtenue sans exception : le parseur a été retouché là où sa forme d’origine portait des bras inatteignables (un itoa maison dont le unwrap_or(0) ne pouvait jamais servir, un buffer.get(..n) dont le None était impossible). Ces bras n’étaient pas des protections : c’étaient des angles morts. Ils ont été remplacés par des formes dont chaque chemin d’erreur est atteignable — et atteint par un test ;
  • fuzz — cible fuzz_air_json_parse (renommée depuis fuzz_air_sshd_json), qui éprouve air_json::parse sur octets arbitraires : jamais de panique, imbrication bornée, round-trip stable ;
  • proptestparse ∘ serialize = identité sur des valeurs arbitraires bornées en profondeur, point fixe de la sérialisation, et « sur des octets arbitraires, parse rend toujours un Result ».

5. Ce que le périmètre reste

Inchangé (ADR-114 §3) : objets, tableaux, chaînes (échappements standards + échappement Unicode BMP), entiers i64, true/false/null. Les nombres fractionnaires et exponentiels restent refusés — Principe 5 : on ne les ajoutera que le jour où un besoin mesuré l’exigera. Deux tolérances héritées sont conservées et documentées plutôt que durcies en silence : les zéros de tête (007) et les clés dupliquées (la 1re gagne). Les durcir serait un changement de comportement, pas une extraction.

Conséquences

  • Un seul codec JSON possédé, fuzzé, partagé — la CLI air-keystore (ADR-115) s’y branchera sans rien réécrire, et le mode JSON agent-safe repose sur un parseur dont les invariants sont prouvés, pas supposés.
  • Aucun ajout à l’arbre du produit : air-json n’a aucune dépendance externe, sa fermeture est exactement celle d’air-base-core. Pas de serde_json en production (ADR-114 §2 ; ADR-024).
  • air-sshd s’allège d’un module qui n’avait pas de raison d’y vivre.

Ce que cet ADR ne fait pas

  • Aucun retrait, aucune signature modifiée dans la couche 1 existante. Pas une primitive scellée n’est touchée.
  • Aucune consolidation du JSON d’air-account-cli. air-account-cli possède son propre module JSON (un écrivain seulement : write_bytes/write_string, qui préserve la fidélité des octets non-UTF-8 d’un nom de compte ou d’un chemin — ce qu’AirJsonValue::String, qui porte un String UTF-8, ne sait pas faire). ADR-114 ne le mentionne pas, et le périmètre de cet incrément est l’extraction depuis air-sshd. La consolidation est remontée comme candidate ultérieure (§Points remontés), pas embarquée ici.
  • Aucune extension du périmètre (pas de flottants, pas de paires de substitution UTF-16, pas de JSON en streaming).
  • Aucune consommation par air-keystore : c’est la vague 2 #3 (ADR-115).

Alternatives rejetées

  • Garder le module dans air-sshd et le ré-exporter. Rejeté : air-keystore est couche 1, air-sshd couche 2 — la dépendance serait à contre-sens du layering (ADR-077), et un binaire d’administration des clés n’a rien à tirer d’un démon SSH.
  • Prendre serde_json. Rejeté par ADR-114 §2, et les trois raisons tiennent : règle des 80 % (ADR-024), os-free (ADR-088), et JSON jamais lu au runtime (ADR-073).
  • Conserver JsonError et le convertir aux frontières. Rejeté : voir §2 — le coût (une conversion à chaque appelant) achèterait une finesse de matching que personne ne demande.

Points remontés au BDFL

  1. Une crate couche 1 de plus, donc un re-sceau. Rien de scellé n’a bougé ; c’est la surface de la couche qui s’étend. Le tag couche-1-v3.6 est à poser après revue.
  2. Le codec avait des bras morts. Trois chemins d’erreur du parseur d’origine étaient inatteignables par construction — ils avaient l’apparence de la prudence sans en avoir l’effet. Ils sont retirés au profit de formes dont chaque chemin est testé. C’est le genre de dette qu’un seuil de couverture à 100 % révèle et qu’un seuil à 96 % laisse dormir : argument de plus pour la barre couche 1.
  3. air-account-cli garde son écrivain JSON. Deux codecs coexistent donc dans l’arbre, avec des contrats différents (fidélité aux octets non-UTF-8 d’un côté, modèle de valeur UTF-8 de l’autre). Une consolidation est possible — elle demanderait à air-json une variante d’octets bruts, c’est-à-dire une extension de périmètre, donc une décision, pas un refactoring. À instruire quand la CLI air-keystore (ADR-115) aura montré ce dont un consommateur a réellement besoin.