Protocole : nestor.satellite/1
Le standard que tout satellite implémente, quel que soit son langage. Version actuelle, la seule que les serveurs acceptent.
Un satellite est un appareil qui s’appaire à un serveur Nestor (le cœur), déclare ce qu’il sait faire, fait passer l’audio, remonte des observations et exécute les intentions du cœur. Cette page est le standard : les SDK et les pilotes, dans tout langage, l’implémentent, et la suite de conformité boîte noire du cœur les vérifie. Voici la version 1, la première (voir l’historique).
Les mots DOIT, NE DOIT PAS, DEVRAIT, NE DEVRAIT PAS et PEUT s’entendent au sens de la RFC 2119 (MUST, MUST NOT, SHOULD, SHOULD NOT, MAY).
Les classes de messages du cœur (nestor.modules.satellites.wire) font foi pour les formes. Les
schémas JSON qu’elles génèrent sont servis à
https://nestorhome.dev/schemas/satellite/1/<message>.json (voir Schémas) ;
les SDK en gardent une copie et testent ce qu’ils envoient avec.
1. Vue d’ensemble
appareil cœur membre du foyer
│ POST /satellites/pairings ────────▶ pending + code d’appareil ◀── lance un ajout (facultatif)
│ présente le code d’appareil (écran, voix, journal)
│ ◀──────────────── tape le code d’appareil
│ ou, au premier arrivé
│ POST …/{pairing_id}/enrollment ───▶ approved (code d’enrôlement créé dans l’administration)
│ GET /satellites/pairings/{id} ───▶ approved + jeton (une fois)
│ POST /satellites/{device_id}/offer (jeton Bearer, SDP WebRTC)
│ canal de données `nestor` : hello → welcome, intentions ⇄ acks, observations
Un appareil configuré avec un code d’enrôlement l’envoie dès sa première requête et il est approuvé aussitôt.
2. Méthodes d’appairage
La preuve qu’une personne voulait appairer un appareil circule dans un sens ou dans l’autre :
- un code d’appareil va de l’appareil à la personne, qui le tape dans l’administration ;
- un code d’enrôlement va de l’administration à l’appareil, qui le présente au cœur.
L’appareil déclare les méthodes qu’il prend en charge ; la personne en choisit une. Les méthodes sont :
| Méthode | Preuve | La personne… |
|---|---|---|
device_code.display |
code d’appareil | lit le code sur l’écran de l’appareil et le tape dans l’administration |
device_code.speech |
code d’appareil | entend l’appareil dire son code et le tape dans l’administration |
enrollment.input |
code d’enrôlement | tape le code d’enrôlement sur l’appareil |
enrollment.provisioning |
code d’enrôlement | met l’URI ou le code d’appairage dans la configuration de l’appareil (option, environnement, fichier) |
enrollment.qr |
code d’enrôlement | montre le QR code de l’URI d’appairage à la caméra de l’appareil |
L’identification (section 6) n’est pas une méthode : elle aide à distinguer les appareils et se
déclare par la capacité device.identify.
2.1 Négociation
- N1. Un manifeste DOIT lister dans
pairing_methodsles méthodes que l’appareil prend en charge, au moins une, sans répétition. Un appareil NE DOIT PAS déclarer une méthode qu’il ne sait pas mener à bien. - N2. Le cœur recommande une méthode, la première que l’appareil prend en charge dans cet
ordre :
device_code.display,device_code.speech,enrollment.qr,enrollment.input,enrollment.provisioning. Pour un appareil qui n’a pas encore contacté le cœur, toutes les méthodes sont proposées. L’administration met en avant la méthode recommandée et garde les autres disponibles. - N3. L’appareil n’impose jamais de méthode. Quoi que la personne choisisse, l’appareil DOIT accepter les deux preuves jusqu’à ce que l’une soit acceptée (section 3.3).
3. Appairage
3.1 Points d’accès
Tous les corps sont en JSON (Content-Type: application/json).
| Requête | Corps | Réponse |
|---|---|---|
POST /satellites/pairings |
pairing.request |
201 pairing.ticket |
POST /satellites/pairings/{pairing_id}/enrollment |
pairing.enrollment |
200 pairing.state |
GET /satellites/pairings/{pairing_id} |
200 pairing.state |
POST /satellites/pairings
{"manifest": {"protocol": "nestor.satellite/1",
"device": {"id": "capteur-entree", "model": "esp32", "room": "entree"},
"capabilities": [{"name": "device.identify"}],
"pairing_methods": ["enrollment.provisioning", "enrollment.qr"]},
"enrollment_code": "K7QF2M9X", "method": "enrollment.provisioning"}
201 {"pairing_id": "…", "status": "approved", "expires_at": "2026-10-30T12:10:00+00:00"}
201 {"pairing_id": "…", "status": "pending", "expires_at": "…", "device_code": "042917"} (sans code)
GET /satellites/pairings/{pairing_id}
200 {"status": "pending", "identify": true}
200 {"status": "approved", "token": "nst1_…", "device_id": "capteur-entree", "room": "entree",
"capabilities": ["device.identify"]}
Les erreurs répondent pairing.error : {"error": code, "detail": texte, "reason"?: …, "retry_after_s"?: …}.
error |
Statut | Sens |
|---|---|---|
invalid_request |
422 | le corps ne suit pas son schéma |
protocol_unsupported |
422 | le manifeste nomme une autre version de ce protocole |
too_many_pending |
429 | trop d’appareils attendent une preuve ; Retry-After et retry_after_s disent quand réessayer |
too_many_attempts |
429 | trop de codes d’enrôlement faux ; Retry-After et retry_after_s disent quand réessayer |
enrollment_refused |
403 | le code d’enrôlement n’appaire rien ; reason vaut unknown, expired, used ou cancelled |
not_pending |
409 | l’appairage n’attend plus de preuve : l’autre preuve l’a emporté, ou il a expiré |
detail est en anglais, pour les journaux. Un appareil qui montre ou dit une erreur aux gens DOIT
la traduire à partir de error et reason, jamais afficher detail.
3.2 États d’un appairage
code d’appareil tapé dans l’administration
┌───────────────────────────────┐
requête ──▶ pending ├──▶ approved ──(première lecture : jeton)──▶ claimed
│ └───────────────────────────────┘ │
│ code d’enrôlement accepté │ l’échéance passe, jeton non réclamé
│ ▼
└── avec un code d’enrôlement ──▶ approved expired ◀── l’échéance passe en attente,
ou l’appareil redemande (remplacé)
- Un appairage en attente dure 10 minutes. Un
pairing_idinconnu, remplacé ou oublié se litexpired: interroger ne révèle rien. - Le jeton n’est remis qu’avec la première réponse
approved; les lectures suivantes donnentclaimed.
3.3 Règles de l’appareil
- P1. Un appareil DOIT prendre en charge
enrollment.provisioning: il lit une URI d’appairage dans la variable d’environnementNESTOR_PAIRING_URI, ou un code d’enrôlement dansNESTOR_PAIRING_CODEavec l’adresse du cœur donnée à part, et DEVRAIT accepter les deux par une option de ligne de commande et dans son fichier de configuration. - P2. Sans code d’enrôlement, un appareil DOIT demander un appairage et présenter son code
d’appareil sur toutes ses sorties : à l’écran (
device_code.display), à voix haute (device_code.speech: une courte phrase, puis les chiffres un par un avec une pause entre chacun, le tout dit deux fois, dans la langue de l’appareil, sans avoir besoin du réseau), et toujours dans son journal. Un appareil sans écran ni voix DOIT écrire le code d’appareil dans son journal et y dire qu’un code d’enrôlement est le moyen de l’appairer. Il interroge ensuite l’appairage environ une fois par seconde jusqu’à ce qu’il soit approuvé ou expire ; s’il expire, il redemande. - P3. Un appareil qui a un code d’enrôlement DOIT l’envoyer avec sa requête, avec la méthode par laquelle il l’a reçu, et NE DOIT PAS présenter de code d’appareil sauf si ce code est refusé.
- P4. Un appareil en attente DOIT accepter un code d’enrôlement à tout moment (tapé, scanné,
configuré) et l’envoyer à
POST …/{pairing_id}/enrollment. La première preuve acceptée l’emporte :- si le code d’appareil a été tapé d’abord, l’enrôlement répond
not_pendinget le code d’enrôlement n’est pas consommé : l’appareil continue d’interroger et obtient son jeton ; - si le code d’enrôlement a été accepté d’abord, le code d’appareil tapé ensuite est refusé.
Sur
approved, l’appareil cesse aussitôt de présenter son code d’appareil (une voix se tait).
- si le code d’appareil a été tapé d’abord, l’enrôlement répond
- P5. Un code d’enrôlement refusé par
enrollment_refusedNE DOIT PLUS être présenté, même après un redémarrage : l’appareil garde les codes refusés avec son jeton (une empreinte de chacun suffit). Il DOIT le dire dans son journal (et sur ses sorties, traduit), puis se rabattre sur un code d’appareil (P2) pour qu’une personne puisse encore l’appairer. - P6. Sur un
429, un appareil DOIT attendre au moinsRetry-Aftersecondes avant de réessayer. - P7. Les codes d’enrôlement DOIVENT être normalisés avant envoi (section 4.2) ; un appareil PEUT les envoyer tels que tapés, le cœur normalise aussi.
- P8. Un appareil DOIT garder son jeton secret (lisible par son seul propriétaire sur un système
de fichiers) et l’oublier quand le cœur le refuse (
401sur une offre), puis s’appairer à nouveau.
3.4 Règles du cœur
- C1. Le cœur n’accepte que
nestor.satellite/1. Un manifeste qui nomme un autrenestor.satellite/<n>est refusé avecprotocol_unsupported. - C2. Un code d’enrôlement appaire un seul appareil : il est consommé dès qu’il est accepté,
avant que quoi que ce soit puisse l’observer, si bien que deux appareils qui l’utilisent en même
temps obtiennent une approbation et un
enrollment_refused(used). - C3. Une preuve n’est acceptée que tant que son appairage est
pending; la première acceptée le fait passer aussitôt àapproved. Un code d’enrôlement refusé parce que l’appairage n’est plus en attente n’est pas consommé. - C4. Le jeton fait 256 bits aléatoires, préfixé
nst1_; seul son SHA-256 est conservé. - C5. Une nouvelle demande avec le même identifiant d’appareil remplace la demande en attente.
- C6. Le journal enregistre chaque étape :
satellite.pairing.requested(avec les méthodes déclarées),satellite.addition.started|completed|cancelled|expired,satellite.enrollment.refused(avec sa raison et l’adresse),satellite.paired(avec la preuve, la méthode et l’ajout),satellite.identify.requested,satellite.revoked. Jamais un code ni un jeton en clair.
3.5 Ajouts
Une personne ajoute un satellite depuis l’administration en lançant un ajout : la méthode choisie et, si elle le veut, un nom, une pièce et les capacités à autoriser au plus. Il dure 15 minutes par défaut (de 1 minute à 7 jours).
- Avec une méthode d’enrôlement, l’ajout crée le code d’enrôlement, rendu une seule fois avec son URI d’appairage. L’appareil qui le présente est appairé aussitôt, nommé, dans la pièce de l’ajout, avec les capacités déclarées restreintes à celles de l’ajout.
- Avec une méthode par code d’appareil, l’ajout attend un appareil : l’administration liste les appareils en attente, la personne les distingue (identification), tape le code que présente le bon, et le nom, la pièce et les capacités de l’ajout s’appliquent.
Un ajout se termine quand son appareil est appairé (completed), quand une personne l’annule ou
quand il expire. Il y en a au plus 32 en cours à la fois. Un ajout appaire un seul appareil : le
cœur le réclame en une étape, si bien que deux personnes qui tapent un code pour lui au même instant
appairent un seul appareil, et l’autre apprend que l’ajout est terminé.
4. Codes et URI
4.1 Code d’appareil
Six chiffres décimaux (042917), aléatoires, uniques parmi les appairages en attente. Des chiffres
se lisent bien à voix haute et sur n’importe quel écran.
4.2 Code d’enrôlement
- Huit symboles de l’alphabet base32 de Crockford
0123456789ABCDEFGHJKMNPQRSTVWXYZ: 40 bits aléatoires. - Affiché par groupes de quatre :
K7QF-2M9X. - Normalisation, avant toute comparaison : retirer espaces et tirets, passer en majuscules, puis
remplacer
Opar0,IetLpar1. Un résultat qui ne fait pas 8 symboles de l’alphabet n’est pas un code (Un’en est jamais un). - Forme canonique : les 8 symboles normalisés, sans tiret.
4.3 URI d’appairage
nestor+http://nestor.local:7860/?code=K7QF2M9X
nestor+https://nestor.example.org/satellites-proxy/?code=K7QF2M9X
- Le schéma est
nestor+suivi du schéma de l’URL de base du cœur (httpouhttps) ; hôte, port et chemin sont ceux de l’URL de base du cœur. Retirernestor+et la requête donne l’URL de base. codeest le code d’enrôlement, sous forme canonique.- Pourquoi cette forme : l’adresse du cœur et le code voyagent ensemble, un appareil sans écran n’a
besoin de rien d’autre ; le transport (TLS ou non) est explicite ; tout analyseur d’URL la lit ;
le préfixe dit à un lecteur de QR code ou à un chargeur de configuration ce qu’elle contient,
comme
git+https. - L’administration renvoie l’URI ; une interface dessine son QR code elle-même.
5. Manifeste
{
"protocol": "nestor.satellite/1",
"device": { "id": "salon", "model": "reachy-mini", "room": "salon", "brand": "pollen-robotics" },
"sdk": { "name": "nestor-satellite-python", "version": "0.1.0" },
"capabilities": [{ "name": "audio.capture" }, { "name": "audio.playback" }, { "name": "device.identify" }],
"pairing_methods": ["device_code.speech", "enrollment.provisioning"]
}
id et room sont des identifiants en minuscules (^[a-z0-9][a-z0-9_-]{0,63}$). Les capacités
sont des noms famille.fonction, 32 au plus, chacune déclarée une fois. Capacités connues :
audio.capture, audio.playback, audio.speech_activity, vision.person_detection,
expression.emotion, motion.head, device.identify. Le manifeste est envoyé à l’appairage et
dans hello.
6. Identification
- I1. Un appareil qui déclare
device.identifyDOIT, sur une réponse d’attente portant"identify": true, se faire remarquer quelques secondes avec les sorties dont il dispose (clignoter, remuer, sonner, faire flasher son écran), et continuer d’interroger. - I2. Une fois appairé et autorisé à
device.identify, il DOIT accuser réception deintent.identifycomme de toute intention, et se faire remarquer de la même façon. - I3. Le cœur n’envoie l’identification qu’à un appareil qui l’a déclarée (en attente) ou qui y est autorisé (appairé). L’administration la refuse sinon, ainsi que pour un appareil appairé qui n’est pas connecté.
- L’identification aide seulement une personne à distinguer des appareils. Elle ne prouve rien : n’importe quel appareil peut clignoter sur demande, elle ne remplace donc jamais un code.
7. Lien temps réel
- W1. Avec son jeton, l’appareil propose une connexion WebRTC :
POST /satellites/{device_id}/offeravecAuthorization: Bearer <jeton>et{"sdp", "type"}; le cœur répond{"sdp", "type", "pc_id"}. Les candidats ICE suivent enPATCHsur le même chemin. Un mauvais jeton répond401; lepc_idd’un autre appareil,403. - W2. Sur le canal de données
nestor, chaque message est{"type", "id", "at", "payload"}. L’appareil DOIT envoyerhello(son manifeste, avec l’identifiant appairé) en premier ; le cœur répondwelcome(la pièce appairée et les capacités autorisées). L’appareil DOIT accuser réception de chaqueintent.*par unacken moins d’une seconde, et NE DOIT remonter que les observations qui lui sont permises. Les types de message inconnus venant du cœur sont ignorés. - W3. Audio : Opus sur WebRTC, la voix du cœur vers l’appareil, le micro (avec
audio.capture) vers le cœur. - W4. Après un raccrochage ou une perte du lien, l’appareil se reconnecte avec le même jeton, avec un délai croissant.
8. Sécurité
- Entropie. Un code d’enrôlement a 40 bits (environ 1,1 × 10¹² valeurs). Avec au plus 32 codes
en cours et au plus 5 codes faux par minute et par adresse (20 toutes adresses confondues), les
300 essais qui tiennent dans une durée de vie de 15 minutes en trouvent un environ une fois sur
10⁸ ; en trouver un demande en moyenne quelque 1 600 ans d’essais. Un code d’appareil a 20 bits,
mais il n’est tapé que par une personne connectée, sur un appairage en attente qu’elle a choisi
(
pairing_id), dans les 10 minutes. - Durée de vie. Les codes d’enrôlement durent 15 minutes par défaut, 7 jours au plus ; les codes d’appareil, 10 minutes.
- Usage unique. Un code d’enrôlement accepté est consommé avant que la réponse soit écrite (C2).
- Limites de débit. Les codes d’enrôlement faux sont comptés par adresse cliente et au total sur
une minute glissante. Au-delà de l’une ou l’autre limite, chaque tentative, juste ou fausse, est
refusée avec
429jusqu’à ce que la fenêtre libère une place : un client limité n’apprend rien. - Empreintes. Seuls les SHA-256 des codes et des jetons sont stockés, et journalisés. Un code présenté est comparé à chaque empreinte connue en temps constant. 40 bits se cassent hors ligne à partir d’une empreinte divulguée : l’empreinte ne vaut que tant que le code vit, gardez des durées de vie courtes.
- Transport. Codes et jetons traversent le réseau en clair sur
http; utilisezhttps(une URInestor+https) hors d’un réseau domestique de confiance. - Ce que l’identification ne prouve pas. Elle montre quel appareil répond, pas que c’est celui que la personne a en main : n’appairez jamais sur la seule identification.
- Consentement. L’appairage ne donne à un appareil que les capacités qu’une personne a autorisées, jamais plus que ce qu’il a déclaré.
9. Conformité
check_satellite(driver) lance le pilote une fois par scénario : une fonction coroutine Python qui
prend un Launch, ou une commande où {url} est l’URL du cœur et {state} un dossier neuf pour
son jeton. Le code d’enrôlement est donné dans NESTOR_PAIRING_URI (retirée de l’environnement des
autres scénarios). Un pilote, dans n’importe quel langage, lance la suite en ligne de commande :
voir Conformité.
| Vérification | Règles |
|---|---|
| déclare enrollment.provisioning | N1, P1 |
| demande à s’appairer ; réclame son jeton une fois son code d’appareil tapé | P2 |
continue d’interroger après une identification en attente (s’il déclare device.identify) |
I1 |
| se connecte avec son jeton ; envoie d’abord un hello ; se présente comme l’appareil appairé | W1, W2 |
accuse réception de chaque intention en moins d’1 s (intent.identify s’il y est autorisé) |
W2, I2 |
| chaque message suit son schéma | W2 |
| reçoit l’audio du cœur ; envoie l’audio de son micro | W3 |
| se reconnecte avec le même jeton après un raccrochage | W4 |
s’appaire avec le code d’enrôlement de NESTOR_PAIRING_URI, sans code tapé |
P1, P3 |
| se rabat sur un code d’appareil après le refus de son code d’enrôlement (faux, expiré, utilisé) | P5 |
| ne présente qu’une fois un code d’enrôlement refusé (faux, expiré, utilisé) | P5 |
| attend Retry-After quand il est limité ; s’appaire une fois la limite levée | P6 |
Les règles propres au cœur (C1 à C6, la course de P4, I3, les limites) sont prouvées par ses tests
(tests/modules/satellites/,
test_gateway.py).
10. Historique
- 1. Première version. Appairage dans les deux sens : codes d’appareil (affichés, dits ou
journalisés) et codes d’enrôlement (tapés, configurés, scannés) créés par une personne qui ajoute
un satellite depuis l’administration, avec l’URI d’appairage. Identification, schémas JSON pour
chaque message, erreurs avec codes, lien temps réel en WebRTC. Les appareils appairés avec le
brouillon non publié
nestor.satellite/0ne s’authentifient plus : mettez-les à jour, puis appairez-les à nouveau.