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

Instruction — la disposition sur disque d’un bundle, et l’emplacement de l’octroi

Objet. Le dernier manque d’ADR-010 : le mécanisme d’ADR-150 est complet et prouvé — manifeste, octroi, signature, arbitrage, plafond d’image, pose de la cage — mais rien ne relie un .airapp sur disque à spec.granted. AppManifest::read(reader) prend un lecteur, jamais un chemin. C’est l’incrément 6 de la spec couche 2.

TRANCHÉ le 2026-08-12 (BDFL). Les trois questions posées — nom et emplacement du manifeste dans le bundle, chemin de l’octroi hors du bundle, sort de .airservice — sont tranchées telles que proposées ci-dessous. Sous-choix arbitrés dans la foulée : anglais sur le disque (§7.1), /var/lib/air/ comme racine des bundles installés (§7.2), 0600 pour les octrois (§7.3).

Rien n’est encore implémenté. Cette note est ce qu’il faut avoir lu avant d’écrire le code.

1. Ce que le code décide déjà, et qu’il ne faut pas re-trancher

La disposition est plus contrainte qu’il n’y paraît : quatre décisions déjà prises la bornent, et deux d’entre elles ne sont écrites nulle part ailleurs que dans un schéma.

  • La racine est un répertoire, et le manifeste ne peut désigner que dedans. Executable.path est relatif à la racine du bundle, et le lecteur refuse l’absolu comme .. — « un manifeste qui désignerait /usr/bin/sh ou ../../autre-bundle ferait porter à l’octroi d’une application le lancement d’un binaire qui n’est pas le sien » (air-config-bundle.capnp).
  • La signature est un artefact séparé, domaine air.signature, subject = manifest. Signer le manifeste couvre les binaires par transitivité (chacun y porte son SHA-256) — à une condition que le schéma dit en toutes lettres et qu’il faut répéter ici : un exécutable absent du manifeste n’est couvert par rien.
  • L’octroi vit hors du bundle, sur la partition inscriptible, émis à l’installation, indexé par bundleId (ADR-150 D1, spec couche 2 §5ter.6).
  • La convention de placement existe déjà et est tenue partout dans le dépôt : /etc/air/… derrière air-admin-gate (DEFAULT_BASE_DIR), extension .aircfg pour tout artefact sous enveloppe AIRCFGv1.

2. La disposition

<racine>/org.exemple.editeur/
├── manifest.aircfg              artefact du domaine air.manifest
├── manifest.signature.aircfg    artefact du domaine air.signature (subject = manifest)
├── executables/                 les binaires déclarés au manifeste
├── libraries/                   bibliothèques embarquées
└── resources/                   données en lecture seule

Ni bin/ ni lib/. ADR-029 refuse l’abréviation sur toute surface inventée par Air, et celle-ci en est une : un développeur la lira dans chaque bundle qu’il construira. La familiarité de bin vient d’Unix, pas de nous ; l’invoquer serait exactement l’argument qu’ADR-029 écarte.

Trois racines, choisies par l’installateur :

RacineContenuQui écrit
/usr/lib/air/bundles/ce que l’image portepersonne — verity
/var/lib/air/applications/les .airapp installésla porte d’administration seule
/var/lib/air/services/les .airservice installésla porte d’administration, privilégiée

3. Le répertoire ne porte pas le suffixe .airapp

Le répertoire installé s’appelle <bundleId>, sans suffixe. La raison n’est pas esthétique : elle est écrite dans le schéma, à propos du champ kind.

« .airapp ou .airservice. Le second ouvre un modèle d’entitlements étendu et une installation privilégiée ; le distinguer dans l’artefact évite qu’il se déduise du chemin d’installation, qui est plus facile à influencer. »

Nommer le répertoire <bundleId>.airservice rouvrirait précisément la tentation que ce commentaire referme — un lecteur pressé finirait par lire le suffixe plutôt que l’artefact. La distinction est portée par la racine, que choisit la porte d’administration, et par kind, que porte l’artefact signé. Le chemin n’entre jamais dans la décision.

Le suffixe .airapp reste le nom de la forme de distribution — l’archive ou le répertoire qu’un développeur produit et qu’un utilisateur télécharge. L’installation en dépose le contenu sous la racine. Les deux formes n’ont pas à coïncider, ADR-150 D1 voyant de toute façon une mise à jour comme une désinstallation suivie d’une installation.

4. L’octroi

/etc/air/grants/<bundleId>.aircfg              0600 root:root
/etc/air/grants/<bundleId>.signature.aircfg    0600 root:root

/etc/air parce que c’est la partition inscriptible derrière la porte d’administration. L’étude de placement (§3.2) classerait l’octroi en « critique — dans l’image », mais ADR-150 §5ter.6 a déjà tranché l’objection : l’y mettre ferait d’une installation d’application une image nouvelle. Ce qui le protège à la place est la composition de trois choses déjà décidées — signé par la machine, écrivable par la seule porte d’administration, et plafonné par l’enveloppe de l’image à l’étape 3 de l’arbitrage.

0600, et c’est la partie qui mérite d’être justifiée. L’enveloppe air_signed_default accorde la lecture de tout /etc/air à n’importe quelle application signée (ceiling.rs). Landlock ne fait que restreindre : il n’ouvre rien que le DAC ferme, mais il ne ferme rien non plus que le DAC laisse ouvert. Le seul rempart restant est donc le mode du fichier.

Un octroi n’est pas un secret — il énonce ce que la machine autorise, pas comment y accéder. Mais l’ensemble des octrois est la carte de ce que la machine autorise, et ADR-150 D4 a rendu le refus muet précisément pour ne pas offrir d’oracle d’énumération. Un ls /etc/air/grants/ lisible par toute application rendrait D4 sans objet. Rien de non-root n’en a besoin : le lanceur et la porte d’administration sont root l’un et l’autre.

Coût assumé, et il faut le dire : aucun outil d’inspection non privilégié ne pourra montrer à un utilisateur ce que ses applications ont le droit de faire. Cet outil n’existe pas encore ; le jour où il existera, il passera par un service root qui répond à des questions, pas par un droit de lecture sur le répertoire.

5. .airservice partage la disposition — entièrement

Il ne diffère que par trois choses, dont aucune n’est un format : kind dans l’artefact, sa racine d’installation, et le fait que son installation exige la porte privilégiée.

Écrire une seconde disposition donnerait deux lecteurs pour un seul schéma, donc deux occasions de diverger au voisinage d’une décision de privilèges — ce que le Principe 11 cherche à éviter. Et ADR-010 ne demande pas autre chose : il réclame pour .airservice un « modèle d’entitlements étendu », c’est-à-dire un contenu plus large, jamais un autre contenant.

6. Ce que la disposition referme — l’écart EXECUTE

C’est le point le plus important de cette note, et il n’a pas été trouvé en concevant la disposition mais en lisant le code pour la concevoir.

6.1 Le constat

ceiling.rs annonce, pour les deux enveloppes par défaut :

« aucune exécution nulle part — même sur /usr. Un binaire étranger tourne ; il ne lance pas d’autres binaires. C’est ce qui casse la chaîne classique “télécharger puis exécuter”. »

Cette propriété n’est pas appliquée. AirLandlockPolicy::handled_access() (crates/air-sandbox/src/landlock.rs:325) construit le masque handled_access_fs par union des droits des règles présentes. Or aucune enveloppe n’accorde execute, donc aucune règle de cage ne porte EXECUTE, donc EXECUTE n’est pas géré — et Landlock ne borne que ce qu’on lui déclare géré. La catégorie seccomp Exec étant accordée d’office (syscall_categories_for), rien ne s’oppose plus à un execve.

Une application confinée peut donc écrire un binaire dans /var/lib/<bundleId> — que l’enveloppe lui accorde en écriture — et l’exécuter.

Ce que ce n’est pas : une élévation de privilèges. La cage survit à l’execve (no_new_privs, seccomp et Landlock sont hérités) ; le binaire tombe dans la même cage. Ce que c’est : la négation du seuil d’exécution — du code non vérifié s’exécute, ce qu’ADR-046 D6 interdit, et la chaîne « télécharger puis exécuter » n’est pas cassée.

Statut de ce constat : lu dans le code, pas encore démontré par un test. Le premier travail de l’incrément est de l’écrire, et de vérifier qu’il tombe une fois la règle du §6.2 posée. Un constat non reproduit n’est pas un constat — le dépôt s’est déjà fait prendre deux fois à publier un diagnostic plausible et faux.

6.2 La règle d’office, et pourquoi elle n’est pas un octroi

Le lanceur ajoute à la cage, d’office, une règle Landlock EXECUTE par exécutable déclaré au manifeste, résolu sous la racine du bundle — plus la lecture de la racine. Ni le manifeste ni l’octroi n’ont à la demander.

C’est le pendant exact de SyscallCategory::Exec, déjà accordé d’office pour une raison strictement structurelle : une cage posée du dehors s’installe à l’instant qui précède l’execve du service, et un profil qui interdirait execve tuerait tout service sur son propre démarrage. Même raisonnement ici : un bundle qui ne peut pas exécuter son propre binaire n’est pas confiné, il est mort. Ce n’est pas une permission accordée, c’est ce sans quoi la question ne se pose pas.

Et l’effet de bord est la propriété recherchée. Dès qu’une règle porte EXECUTE, EXECUTE entre dans handled_access_fs — et tout le reste du système de fichiers cesse d’être exécutable. La propriété que ceiling.rs annonçait devient vraie, sans qu’aucune enveloppe change.

Par exécutable plutôt que par répertoire. Une règle sur executables/ serait plus simple et plus faible : un binaire déposé après coup dans ce répertoire s’exécuterait. Le manifeste lie déjà chaque exécutable à son SHA-256 ; poser une règle par fichier déclaré rend exécutable exactement ce qui est signé, et rien d’autre. Coût : un O_PATH par exécutable, ouvert dans le parent comme le fait déjà AirPreparedCage::prepare.

6.3 Ce que la mise en œuvre a appris — l’interprète ELF

Écrit après coup, le 2026-08-12, parce que la conception du §6.2 était incomplète et que seule la mesure l’a montré.

Nommer le programme ne suffit pas à un binaire lié dynamiquement. Le noyau ouvre aussi son interprète ELF (open_exec()), donc sous le contrôle de EXECUTE : un /bin/sh dont seul le chemin est nommé voit son execve refusé (EACCES), et l’enfant sort 127 sans avoir rien exécuté.

Mesuré sur onze montages, dont les trois qui tranchent :

PolitiqueProgrammeRésultat
EXECUTE sur /usr/bin/dash seul/usr/bin/dashrefusé (EACCES)
EXECUTE sur /usr/bin/dash + son interprète/usr/bin/dashs’exécute
EXECUTE sur /bin/busybox seul (statique)/bin/busyboxs’exécute (puis tué par seccomp — un autre sujet)

Ce n’est pas un défaut de la conception, c’est ADR-046 D6 appliqué un cran plus tôt. Les binaires *-linux-air sont statiques : ils n’ont pas d’interprète, et la règle sur le seul programme leur suffit — la troisième ligne du tableau le prouve. Un binaire glibc, lui, ne meurt plus de son premier syscall hors socle : il ne démarre pas. Le seuil d’exécution passe du profil seccomp au refus de Landlock, ce qui est plus tôt et plus net.

Conséquence à porter : un service transitoire encore lié à la glibc devra nommer son interprète pour tourner sous un octroi. La nécessité est visible, datée, et disparaît avec le portage — exactement le régime voulu pour tout ce qui est transitoire. Aucun mécanisme n’est écrit pour l’instant : le premier service transitoire lancé sous octroi le réclamera, et c’est à ce moment-là qu’il faudra choisir entre nommer l’interprète et achever le portage.

7. Les sous-choix, et ce qui les a tranchés

7.1 Anglais sur le disque

Décision BDFL du 2026-08-12 : « on reste et on doit rester en anglais ». Donc grants/ et non octrois/, executables/ et non executables francisé, manifest.aircfg et non manifeste.aircfg.

C’est la règle du dépôt et elle vaut au-delà de cette note : la documentation est en français, les noms sur le disque — comme les identifiants de code, les commandes et les domaines de schéma — sont en anglais. /etc/air/keystore/hostkeys.aircfg, /etc/air/passwd.aircfg, air.grant : l’usage existant était déjà celui-là, il est désormais explicite.

7.2 /var/lib/air/ comme racine des bundles installés

Debian mettrait plutôt /opt. /var/lib est retenu parce que c’est déjà là que l’enveloppe place les données d’application (/var/lib/{bundleId}, ceiling.rs) : une seule racine inscriptible à connaître plutôt que deux.

7.3 0600 pour les octrois

Justifié au §4. Le mode ferme l’énumération et interdit toute inspection non privilégiée.

8. Ce que cette disposition retire à ADR-010

ADR-010 dit le bundle « autonome et déplaçable ». Dans cette disposition, il ne s’exécute que depuis une racine que la porte d’administration contrôle.

La raison est qu’un répertoire à la fois inscriptible par l’utilisateur et exécutable annulerait tout le reste — c’est la forme même de l’attaque que le §6 cherche à fermer. Et la politique de montage l’a déjà tranché de son côté : les supports amovibles sont montés nosuid,nodev,noexec (étude de placement §3.4), si bien qu’un .airapp posé sur une clé USB ne s’exécute de toute façon pas.

Le bundle reste déplaçable comme forme de distribution ; il n’est pas exécutable en place. C’est un rétrécissement réel de la Décision d’ADR-010, et il est acquis (BDFL, 2026-08-12). ADR-010 étant fondateur mais en phase de design pré-ouverture publique, l’édition directe est autorisée (ADR-015) : la phrase mérite d’être amendée dans l’ADR plutôt que corrigée seulement ici.

9. Ce qu’il reste à écrire

#ObjetDépend de
1Le test qui démontre l’écart EXECUTE du §6.1fait, et l’écart est refermé dans le même incrément : un test qui épinglerait le défaut sans le corriger serait un test vert pour la mauvaise raison
2L’ouverture d’un bundle depuis un cheminfaite : BundleLoader::load. Son périmètre s’est réduit à l’instruction : tout le décodage existait déjà dans arbitrate (enveloppe, garde de domaine, Cap’n Proto), et ArbitrationInputs ne demande que quatre tranches d’octets. Le chargeur ne fait donc que des entrées-sorties, et aucune décision
3La résolution de l’octroi par bundleIdfaite, dans le même module : elle est indissociable de la lecture du manifeste, qui est ce qui valide l’identifiant avant qu’il devienne un nom de fichier
4La règle EXECUTE d’officefaite, ancrée sur spec.program : le lanceur n’a encore qu’une seule vérité sur ce qu’il exécute. Elle passera aux Executable du manifeste avec la tâche 2
5Le câblage jusqu’à spec.grantedfait : --bundle <identifiant> + BundleActivator. Trois sous-choix ont dû être tranchés pour y arriver, §10
6aLa clé de signature de la machinefaite : AirMachineKey dans air-keystore, générée au premier démarrage, graine 0600 + clé publique 0640 écrites d’un seul geste
6bL’émission de l’octroifaite : bundle::install::issue_grant. Établit QUI (signature du manifeste) avant de décider QUOI, refuse ce que l’administrateur nomme sans que le manifeste l’ait demandé, émet et signe avec la clé de la machine
6cLe dépôt et l’outil d’administrationfait : air-bundle show/grant/revoke, gate d’ADR-101 sur les seules mutations, octroi écrit en 0600 atomiquement, révocation par retrait. La copie de l’arborescence n’en fait PAS partie — §10.7
7L’amendement d’ADR-010 sur « déplaçable »fait, dans les deux langues, avec la disposition sur disque portée dans la Décision de l’ADR
8Hisser la garde de confiance de fichier en couche 1faite : AirFileTrust dans air-filesystem. Le lanceur supprime son type (couche 2, libre) ; le magasin garde le sien comme surface publique et délègue (couche 1 scellée — le renommer serait une rupture pour un gain cosmétique). Les bits de garde n’existent plus qu’à un seul endroit

Pourquoi la tâche 8 existe. Le chargeur porte sa propre garde de confiance (ArtifactTrust), qui décalque celle d’air-keystore (AirStoreTrust) sans la réutiliser : celle du magasin est paramétrée par une visibilité propre au magasin de clés, et un lanceur n’a pas à dépendre du gestionnaire de clés pour ouvrir un fichier. La duplication est une dette, et elle est nommée dans le code — une garde de sécurité écrite deux fois est une garde qui divergera. Le dépôt s’est déjà fait prendre : la protection de lecture des artefacts avait été écrite là où le besoin s’était fait sentir plutôt que là où le domaine vit, et hostkeys.aircfg s’était retrouvé gardé moins bien que known_hosts.

Le service désigne son bundle par bundleId, jamais par un chemin. Le lanceur résout le répertoire par convention à partir des trois racines du §2. Une configuration qui nommerait le chemin laisserait un administrateur pointer un service vers un autre répertoire — donc séparer l’identité du bundle de son emplacement, alors que toute la chaîne d’arbitrage suppose qu’ils désignent la même chose.

Pas d’installation par utilisateur dans cet incrément. Un bundle installé dans un répertoire inscriptible par son propre utilisateur serait exécutable et modifiable par lui : le §6 ne tiendrait plus. Si le besoin se présente, il se traitera par un octroi et une racine dédiés, jamais en desserrant celle-ci.

10. Trois sous-choix qu’il a fallu trancher pour câbler l’activation

Aucun n’était dans les trois questions posées au BDFL ; tous trois étaient forcés par la tâche 5 — sans eux, aucun service ne peut être arbitré. Ils sont donc pris, écrits et faciles à reprendre.

10.1 La clé de la machine vit dans le magasin, en /etc/air/keystore/machine_public_key

Cette section a d’abord dit le contraire, et elle avait tort. Elle plaçait la clé dans l’image, « du côté couvert par verity ». C’était inimplémentable : [ADR-121] décrit une image anonyme qui génère son matériel de clé au premier démarrage. Une paire propre à la machine ne peut pas avoir sa partie publique dans une image en lecture seule, partagée par toutes les machines. Corrigé le 2026-08-12, décision BDFL.

La paire est générée au premier démarrage, comme la clé d’hôte d’ADR-121, et rangée dans le magasin d’[ADR-108] : la partie privée en artefact secret (0600 root), la partie publique en fichier de clé brut que le lanceur est seul à lire.

Sans enveloppe AIRCFGv1, et c’est délibéré : une clé publique Ed25519 fait 32 octets, ni plus ni moins. Un fichier de taille fixe n’a besoin d’aucun analyseur — on compare une longueur. C’est le Principe 11 à son état le plus pur ; l’enveloppe transporterait une disposition qui n’a pas de variante. L’absence de suffixe .aircfg est le signal que ce n’est pas un artefact de configuration mais du matériel de clé. La longueur est exacte, jamais maximale : plus court serait tronqué, plus long porterait quelque chose qu’on n’a pas regardé.

Deux fichiers plutôt qu’un, la partie publique séparée de la privée : le lanceur n’a besoin que de vérifier, jamais de signer. Un lanceur qui ouvrirait l’artefact secret pour en dériver la clé publique pourrait, sur un défaut, laisser fuir de quoi émettre des octrois. Il est root et pourrait de toute façon lire ce fichier — la séparation ne protège donc pas contre lui, elle protège contre ses bogues.

Ce que la signature d’octroi protège, et ce qu’elle ne protège pas

Il faut le dire, parce que la première rédaction le laissait croire plus fort qu’il ne l’est.

MenaceProtégé ?
Un compte non privilégié fabrique un octroioui — il ne peut pas écrire le magasin
Un octroi valide transporté depuis une autre machineoui — l’autre machine a une autre clé
Un manifeste réécrit pour demander davantageoui — l’empreinte lie l’octroi à ce manifeste
root fabrique un octroinon — qui peut écrire le magasin peut signer

Ce n’est pas un renoncement : c’est la position déjà actée par l’étude de placement — « si l’attaquant est root, une signature locale ne l’aurait pas arrêté non plus ». Ce qui protégerait vraiment est un ancrage matériel, qu’Air n’a pas encore. Le jour où il existera, c’est cette ligne du tableau qui changera, et elle seule.

10.2 Les trois racines sont cherchées dans l’ordre, l’image d’abord

Un bundle porté par l’image est couvert par verity ; une installation ultérieure ne doit pas pouvoir le masquer. « Porte ce bundle » se constate sur le manifeste, pas sur le répertoire — sinon un répertoire vide, créé par erreur ou à dessein, masquerait l’installation valide d’une racine suivante. Les deux propriétés sont tenues par un test chacune.

Le genre déclaré doit concorder avec la racine où le bundle a été trouvé. Ce n’est pas le chemin qui décide du genre — le schéma est explicite là-dessus. Mais une incohérence est soit une erreur d’installation, soit une tentative : refuser est fail-closed et n’accorde jamais rien que l’artefact n’ait dit.

10.3 Tout bundle reçoit l’enveloppe étrangère, la plus étroite

ADR-046 A2 veut que la provenance décide de la largeur. Reconnaître une signature Air suppose de savoir quelles clés sont celles d’Air — un registre qui n’existe pas.

Tant qu’il n’existe pas, tout le monde reçoit foreign_default(). C’est le choix fail-closed : traiter tout le monde en signé Air accorderait à un inconnu ce qui est réservé à ce qu’on a vérifié, tandis que l’inverse ne fait que restreindre. Le mur est tenu par un test — le jour où il tombera, c’est que quelqu’un aura élargi sans le registre.

Conséquence pratique à connaître : un bundle ne peut, pour l’instant, obtenir que la lecture de /usr et son propre /var/lib/{bundleId}. Ni réseau, ni lecture de /etc/air.

10.4 Qui décide ce qui est accordé à l’installation

Décision BDFL du 2026-08-12 : l’administrateur, par un acte explicite.

issue() documentait déjà que proposed est « ce que l’administrateur (ou la politique locale) propose d’accorder » — ni l’un ni l’autre n’existait. Il n’y aura pas de politique locale dans un premier temps, et pas d’octroi automatique.

L’installation montre ce que le manifeste demande et n’accorde que ce que l’administrateur nomme. Rien d’implicite, pas de « tout accorder » par défaut.

Pourquoi ce n’est pas une lourdeur mais le sujet même. Accorder automatiquement l’intersection demande ∩ enveloppe reviendrait à laisser le développeur choisir ses propres privilèges, l’enveloppe étant la seule borne. C’est littéralement « déclarer vaut obtenir » — l’erreur d’Android avant sa version 6, qu’ADR-150 D1 cite nommément comme la raison d’être de l’octroi. Un octroi que personne ne décide n’est pas un octroi, c’est un manifeste recopié.

Une politique locale reste possible plus tard, pour une flotte. Elle devra alors être un artefact signé et versionné, et devenir elle-même une cible à protéger — raison de plus pour ne pas commencer par elle.

10.7 L’outil d’administration n’est pas un copieur de fichiers

air-bundle accorde. Il ne dépose pas les octets d’un bundle : ceux-là arrivent par l’empaquetage — l’image, un .deb, plus tard la logithèque. Un copieur de fichiers de plus n’ajouterait qu’un second chemin par lequel du code arrive sur la machine, c’est-à-dire exactement ce qu’ADR-046 D6 cherche à réduire à un seul. Ce que l’administration possède est l’octroi, pas les octets.

C’est un rétrécissement de ce que la tâche annonçait (« déposer le bundle »), et il est délibéré.

Ce que l’outil fait, et les choix qui s’y voient

  • Trois commandes, dont une seule ne mute pas : show n’est pas gatée. Gater une lecture n’ajouterait qu’une invite là où il n’y a rien à protéger ; ne pas gater une écriture ouvrirait la porte qu’[ADR-101] ferme. Un test tient cette réponse.
  • Le secret ne passe jamais par argv — première ligne de stdin, comme air-account-cli et air-keystore-cli. Un mot de passe en ligne de commande est visible de ps, donc de tout compte de la machine.
  • Un drapeau par droit (--read-path, --write-path, --connect-port…) plutôt qu’un --path /var/lib/x:rw à décomposer. C’est plus long à écrire et il n’y a rien à analyser. Deux drapeaux sur le même chemin donnent une règle portant les deux droits, pas deux règles concurrentes — sinon « lire + écrire » demandé ne couvrirait aucune des deux moitiés.
  • Il n’existe pas de drapeau « tout ce qui est demandé ». Ce serait un acte explicite, donc conforme à la lettre de la décision du §10.4 — mais c’est exactement celui que tout le monde finirait par taper. Flatpak a perdu son modèle ainsi : --filesystem=home est devenu ce que presque toutes les applications réclament. Si nommer les droits un par un se révèle intenable, ce sera une donnée mesurée, pas une commodité offerte d’avance. Un test vérifie qu’aucun raccourci de ce genre n’est accepté.
  • Une crate à part, air-bundle-cli, et non une sous-commande d’air-launchd : le lanceur vérifie des octrois, il n’en émet pas. Lui donner la clé de signature de la machine dans son arbre de dépendances ferait tourner en permanence, en root, un processus qui porte de quoi accorder. Même partage que air-keystore-cli face à air-sshd.

L’ordre des écritures, et ce qu’il coûte de le prendre à l’envers

À l’écriture, la signature d’abord : si la seconde échoue, il reste une signature sans octroi — l’étape 2 de l’arbitrage échoue sur un octroi absent, c’est-à-dire une révocation, l’état sûr. L’ordre inverse laisserait un octroi que personne n’a signé.

Au retrait, l’octroi d’abord — l’inverse, pour la même raison : l’état intermédiaire doit être celui qui refuse.

Révoquer ce qui n’est pas accordé n’est pas une erreur. Refuser obligerait l’administrateur à savoir avant d’agir, donc à lire le magasin pour pouvoir le nettoyer.

10.5 Nommer un droit non demandé est un refus, pas un silence

issue() intersecte : proposer plus que ce que le manifeste demande rend simplement moins. C’est la bonne propriété pour l’invariant — l’octroi ne peut jamais dépasser la demande, quoi qu’on propose.

Mais le silence serait mauvais ici. Un administrateur qui écrit /etc/shadow sur un bundle qui ne l’a jamais demandé croirait l’avoir accordé, et repartirait avec une idée fausse de ce que la machine autorise. issue_grant refuse donc, en nommant tout ce qui cloche — pas la première erreur, toutes, sinon on corrige une ligne à la fois.

Ce n’est pas une garde de sécurité de plus : l’invariant tient sans elle. C’est une garde contre une méprise, et une méprise sur des privilèges finit par coûter aussi cher qu’une faille.

La comparaison est celle de l’invariant — égalité de chemins et de verbes, jamais inclusion. Un droit d’écriture proposé sur un chemin demandé en lecture seule est « non demandé » : c’est un droit de plus, pas une nuance.

10.6 Ce que l’outil d’administration devra faire, et qui n’est pas encore écrit

Le dépôt reste à faire, et il n’est pas anodin — d’où sa séparation :

  • il passe par la porte d’ADR-101 : toute mutation de configuration Air redemande le mot de passe administrateur, et émettre un octroi en est une. air-admin-gate existe déjà et sera consommé tel quel ;
  • copier une arborescence en imposant les modes a des cas limites propres (liens symboliques dans la source, propriété, atomicité) qui méritent leurs tests plutôt qu’un coin de PR ;
  • la révocation est le retrait de l’octroi — donc une opération de l’outil, pas un mécanisme de plus.

Licence du document : MPL 2.0