nono exécute des agents IA non fiables dans un sandbox imposé par le noyau. Rien n'est accessible sans une capability qui l'accorde explicitement ; et une fois les murs dressés, aucun processus à l'intérieur ne peut les abattre.
Les agents de coding lisent des fichiers, en écrivent, exécutent des commandes et appellent le réseau ; des milliers de fois par session, plus vite que vous ne pouvez relire. Une prompt injection, une dépendance empoisonnée ou un simple bug peut rendre chacune de ces actions hostile.
Les garde-fous applicatifs (« ne touche pas à ~/.ssh, s'il te plaît ») vivent à l'intérieur du processus qu'ils surveillent ; quiconque contrôle l'agent contrôle le garde-fou. nono descend la frontière au niveau du système d'exploitation : le noyau lui-même refuse le syscall. L'agent ne se tient pas bien parce qu'on le lui a demandé gentiment ; il se tient bien parce que refusé est l'état physique par défaut de son monde, et que chaque chemin ou port accordé est une exception délibérée et auditable.
Les deux grands noyaux desktop offrent nativement à un processus le moyen de se verrouiller lui-même avant de faire quoi que ce soit de risqué. nono est un orchestrateur de ces deux primitives ; il n'invente aucune enforcement qui lui soit propre.
Un processus construit un ruleset de permissions sur le système de fichiers (et, sur les noyaux récents, sur le réseau), puis demande au noyau de s'y restreindre. À partir de ce syscall, les règles s'appliquent au processus et à tous les enfants qu'il créera jamais.
Un processus remet au noyau un profil, un petit programme de type Scheme qui part de (deny default) et énumère les exceptions. nono génère ce profil à partir de vos capabilities et échappe chaque valeur qui y entre.
/etc est en réalité /private/etc ; les profils doivent couvrir les deux visages de chaque alias, et la canonicalisation des chemins de nono s'en charge.Sur les deux plateformes, appliquer le sandbox est une porte à sens unique. Aucune API ne permet d'élargir un sandbox vivant depuis l'intérieur ; ni pour nono, ni pour l'agent, ni pour du code que l'agent aurait piégé. Chaque règle est héritée par tous les descendants. Cette unique garantie du noyau est ce qui permet à nono de tenir des promesses qui survivent à un agent totalement compromis.
L'intention descend et se concrétise à chaque étape : les flags deviennent des capabilities, les capabilities deviennent des règles noyau. Rien ne remonte ; une couche basse ne demande jamais la permission à une couche haute.
nono run --profile node-dev -- claude : un nom de profil, quelques flags, une commande à confiner.
Lisez la page de bas en haut si vous évaluez les garanties (que peut réellement promettre le noyau ?), de haut en bas si vous découvrez le code (où irait ma modification ?).
La décision la plus structurante de nono : la bibliothèque sait comment sandboxer, seule la CLI sait quoi sandboxer. Si la bibliothèque embarquait ne serait-ce qu'un seul « défaut raisonnable », chaque intégrateur hériterait d'une politique invisible qu'il n'a jamais relue.
Déterministe, embarquable, sans politique. Tout ici répond à « comment imposer exactement ceci ? »
nono why.Opinionnée, tournée vers l'utilisateur, remplaçable. Tout ici répond à « que faut-il autoriser, et comment le dire à l'humain ? »
Chaque profil, flag, pack et groupe de politique ; quelle que soit sa forme d'expression ; doit se réduire à cette unique valeur explicite et inspectable avant toute application. La FFI C expose exactement ce contrat aux autres langages, et aucune opinion de la CLI ne l'accompagne.
nono runDu clavier à l'agent sandboxé en deux phases : une phase décider, hors sandbox, qui transforme l'intention en CapabilitySet, puis une scission : un processus reste dehors comme superviseur, l'autre entre pour toujours.
Les flags sont parsés ; le profil nommé (intégré, fichier utilisateur ou pack installé) est chargé et fusionné avec la ligne de commande.
Les groupes de politique s'étendent en grants concrets. Chaque chemin est canonicalisé et doit exister. Un grant qui chevauche une règle deny, ou qui touche le répertoire d'état de nono, est une erreur fatale ; pas un avertissement.
Les secrets sont résolus depuis le keystore vers la mémoire. Si la politique réseau filtre le trafic, le proxy démarre maintenant et le CapabilitySet est réduit à « loopback, un port ». Si le rollback est actif, un instantané de référence du workspace est pris.
Tout ce qui exige un parent vivant ; proxy, rollback, élévation interactive, diagnostics ; sélectionne Supervised. Sinon Direct applique le sandbox sur place et exec : nono disparaît et seuls les murs restent.
deux processus existent désormais, aux destins très différents
execve() remplace l'enfant par la commande réelle. L'agent naît déjà entre les murs, les transmet à chacun de ses sous-processus, et ne peut pas leur ordonner de bouger.Une seule question décide de tout : quelque chose doit-il continuer à vivre hors du sandbox après le démarrage de l'agent ?
Le sandbox est appliqué au processus courant, puis exec le remplace par votre commande. nono cesse d'exister ; pas de superviseur, pas de processus supplémentaire, aucune surface d'attaque au-delà des règles noyau elles-mêmes.
Le compromis : sans personne hors des murs, rien de ce qui exige un dehors ne peut fonctionner.
Le défaut pour le travail d'agent. Le parent reste dehors comme superviseur durci ; seul l'enfant est confiné. Tout l'interactif et le forensique vit ici : le proxy réseau, les instantanés de rollback, les invites « autoriser ce fichier ? », et l'explication post-run de chaque refus.
Le compromis : un processus privilégié demeure, donc il se défend (attachement de débogueur bloqué, échec de durcissement fatal).
Le workspace Cargo reflète l'architecture : tout dépend de la bibliothèque nono ; la bibliothèque ne dépend d'aucun des autres.
Cette forme est une garantie, pas une commodité : la bibliothèque ne pouvant pas voir la CLI, aucune politique ne peut fuir vers le primitif, et quiconque embarque nono (via Rust ou la FFI C) part d'un mécanisme réellement neutre.
Le sandbox dit non. Ces sous-systèmes rendent cela vivable : réparer les dégâts, atteindre le réseau sans danger, partager la politique, garder les secrets secrets, et comprendre chaque refus.
Avant que l'agent ne tourne, le contenu de chaque fichier suivi est haché dans un object store adressé par contenu, et un arbre de Merkle scelle tout le workspace sous un unique hash racine. Après le run, une seconde passe montre exactement ce qui a changé ; et peut tout remettre en place, atomiquement.
L'adressage par contenu signifie qu'un contenu identique n'est stocké qu'une fois, que les instantanés sont bon marché, et que la corruption saute aux yeux : re-hacher un objet et comparer. La racine de Merkle réduit « quelque chose a-t-il changé ? » à une seule comparaison.
Les exclusions tiennent les répertoires de VCS (.git et consorts) et les globs configurés par l'utilisateur hors du suivi, pour qu'une restauration ne se batte jamais contre votre gestion de versions. Les fichiers temporaires de sauvegarde atomique des éditeurs sont suivis à part pour qu'une restauration ne laisse pas d'orphelins.
Les primitives (object store, arbre de Merkle, gestionnaire d'instantanés, filtre d'exclusion) vivent dans la bibliothèque ; la CLI possède le cycle de vie : quand snapshotter, comment parcourir les sessions, et la restauration interactive.
Sous politique réseau, le sandbox n'autorise qu'une seule destination : loopback, un port ; là où le proxy écoute, dans le superviseur. Chaque requête de l'agent doit passer devant lui.
Filtrage d'hôtes : les tunnels HTTPS sont vérifiés contre votre allowlist plus une denylist intégrée (endpoints de métadonnées cloud et similaires). Les proxys d'entreprise peuvent être chaînés derrière ; la denylist de nono s'applique toujours comme plancher.
Injection de credentials : l'agent appelle une route locale sans vrai secret ; le proxy attache la véritable clé d'API (ou signe la requête, p. ex. AWS SigV4) au passage. Un environnement d'agent qui fuite ne fuite rien d'utilisable.
L'interception TLS, quand des règles au niveau domaine exigent de lire dans le HTTPS, utilise une CA de session générée à chaque run ; reconnue par ce seul sandbox, jetée ensuite.
Les profils voyagent en packs tirés d'un registre. Parce qu'un pack décide littéralement de ce qu'un agent peut toucher, les packs sont signés et vérifiés avant usage.
--insecure explicite et avertit bruyamment.Pas de dégradation silencieuse : une fois une clé de confiance configurée, rien (ni fichier de flag, ni variable d'environnement) ne peut discrètement retirer la vérification ; seul le flag insecure explicite le peut, et il s'annonce.
La confiance est graduée par origine : les échappatoires comme les règles Seatbelt brutes ne sont honorées que dans les profils que vous avez écrits ; tout pack ou profil de registre en est purgé avant usage, car une telle règle peut tout accorder.
Un profil nomme les secrets par référence ; op://, keyring://, apple-password://, env:, file:, cmd: ; résolus hors du sandbox au lancement et injectés comme variables d'environnement. Aucun secret n'apparaît jamais sur une ligne de commande ou dans un log.
En mémoire, les valeurs vivent dans des wrappers zéroïsants : effacées à la destruction, pas laissées à un core dump. En sortie, chaque schéma a une forme caviardée, pour que les diagnostics puissent dire quel credential sans jamais le montrer.
Combiné au mode injection du proxy, l'agent peut recevoir un simple placeholder pendant que le vrai credential n'existe que dans le superviseur ; la posture la plus forte pour du code non fiable, détaillée ci-dessous dans le motif du jeton fantôme.
Injecter un secret en variable d'environnement est une bonne hygiène, mais le secret vit toujours à l'intérieur du sandbox ; un agent qui peut lire son propre environnement peut le faire fuiter. Le motif du jeton fantôme retire complètement le secret du sandbox : l'agent détient un leurre limité à la session, et l'échange contre le vrai credential se fait dans le superviseur, au passage vers l'extérieur.
Au lancement, le superviseur résout les références de credentials (op://, keyring://…) en mémoire. La vraie clé naît hors des murs et ne les franchit jamais.
L'environnement de l'agent reçoit un jeton fantôme et une URL de base en loopback. Pour le SDK de l'agent, c'est une clé d'API parfaitement normale ; elle n'authentifie qu'auprès du proxy de cette session.
À chaque requête, le proxy valide le fantôme, le remplace par le vrai credential, et transmet. Il peut même signer les requêtes sortantes (refresh OAuth2, AWS SigV4) pour qu'aucune clé longue durée n'existe sur le réseau.
Supposez le pire : la prompt injection gagne, l'agent déverse tout son environnement à un attaquant. Celui-ci obtient un jeton invalide hors du loopback de cette session, qui meurt à la fin du run. Il n'y avait rien à voler.
Les refus ne sont pas des échecs silencieux. Le superviseur enregistre chacun d'eux, et à la fin du run imprime un footer : ce qui a été bloqué, pourquoi, et le flag ou grant exact qui l'autoriserait la prochaine fois. Itérer sur un profil est une boucle de feedback, pas de l'archéologie.
nono why répond aux questions de permission sans rien exécuter, en rejouant un état de capabilities sauvegardé à travers la même logique de résolution que celle du sandbox ; le moment du grant et le moment de la question ne peuvent jamais diverger.
L'apprentissage depuis les refus boucle la boucle : les grants qui manquaient à un run peuvent être sauvegardés directement dans le profil utilisé.
Une ABI C avec header généré expose le contrat de la bibliothèque ; construire un capability set, appliquer le sandbox, interroger les permissions, sérialiser l'état, lire les diagnostics ; depuis tout langage capable d'appeler du C.
La surface est délibérément la moitié mécanisme seulement : aucun des profils, packs, proxy ou groupes de politique de la CLI ne franchit la frontière. Un intégrateur reçoit le même primitif neutre sur lequel la CLI elle-même est construite, et apporte sa propre politique.
Ce ne sont pas des lignes directrices ; ce sont des propriétés contre lesquelles chaque changement est vérifié. Si une fonctionnalité ne peut pas être construite sans en plier une, la fonctionnalité perd.
Aucune API n'élargit un sandbox vivant. L'expansion n'existe qu'à travers des canaux médiés : une invite d'élévation interactive traitée par le superviseur, ou un jeton d'extension macOS pour un chemin précis. Jamais en silence.
Si une règle ne peut pas être imposée exactement comme demandé ; noyau ancien, fonctionnalité manquante, config impossible à charger ; nono s'arrête en erreur plutôt que de tourner affaibli. Il n'y a pas de mode « best effort » pour la sécurité.
Les chemins sont résolus et vérifiés à l'existence au moment du grant, puis revalidés à chaque rechargement d'état stocké. Les échanges de symlinks, les visages d'alias comme /etc vs /private/etc et les astuces de préfixes de chaînes meurent ici.
Une capability non accordée n'existe pas. Les groupes accordent au minimum, lecture et écriture sont des décisions séparées, et le répertoire d'état de nono ne peut jamais être accordé ; l'agent ne peut réécrire ni sa propre histoire ni la mémoire de son geôlier.
La bibliothèque applique exactement le CapabilitySet qu'on lui donne. Chaque opinion ; listes deny, commandes dangereuses, profils ; vit dans la CLI, sous forme de données que vous pouvez lire et auditer.
Le seul processus resté hors des murs bloque l'attachement d'un débogueur à l'instant du fork, et traite l'échec du durcissement comme fatal : tuer l'enfant, avorter le run.
Les credentials se résolvent en mémoire, se zéroïsent à la destruction et n'apparaissent dans les logs que caviardés. Avec l'injection par proxy, la vraie valeur n'entre jamais dans le sandbox.
Les règles plateforme brutes peuvent tout accorder, elles ne sont donc honorées que depuis les profils que vous avez écrits vous-même. Tout ce qui vient d'un pack ou d'un registre en est purgé avant usage.
Retenez cela, plus une image : un agent dans une pièce scellée où chaque porte a été posée à dessein. Tout le reste dans nono ; profils, packs, proxy, rollback, diagnostics ; existe soit pour bien choisir ces portes, soit pour vivre élégamment avec les murs.