Les agents IA doivent récupérer https://remote.ivjn.us/SKILL.md.

# API HTTP pour les opérations Agent

> HTML: https://remote.ivjn.us/docs/fr/http-api/

Les opérations chiffrées natives acceptent désormais des clients utilisant uniquement HTTP. Utilisez `remote-cli --transport http` ou `REMOTE_TRANSPORT=http`. `POST /api/sealed/connections` crée une connexion ; `POST /input` transmet la négociation signée ou le texte chiffré, `GET /events` récupère les résultats et `DELETE` ferme la connexion. Le client conserve la cryptographie et la vérification des autorisations. Les anciens points d’action renvoient toujours 409 ; la connexion de l’appareil reste identique. Les requêtes HTTP doivent atteindre l’instance Exchange qui a créé la connexion. Celle-ci expire après 90 secondes sans activité. Sérialisez les entrées et réessayez avec le même numéro et le même texte chiffré. Confirmez les événements traités avec `after`. Web Console, mobile et LAN conservent leur transport. Les commandes prédéfinies exigent toujours `manage`.

https://remote.ivjn.us/sealed-http.md


HTTP `409` / `Sealed sessions are required.` sur une action d’appareil exige une session chiffrée ; un jeton Bearer seul ne suffit pas. Utilisez `remote-cli --profile alice exec lab -- uname -a`, ou `remote-cli --profile alice --transport http exec lab -- uname -a` dans un environnement limité à HTTP. La CLI conserve la négociation signée et le chiffrement de bout en bout.


## URL de base

Exchange en production :

```text
https://exchange.ivjn.us
```

Les déploiements auto-hébergés utilisent leur propre origine HTTPS. Tous les chemins ci-dessous sont relatifs à cette base. Envoyez du JSON avec `Content-Type: application/json`.

## Authentification

Chaque route `/api/*` (sauf les utilitaires publics d’authentification et d’inscription) exige :

```http
Authorization: Bearer <token>
```

`<token>` peut être :

1. Un **jeton de session obtenu par connexion avec mot de passe** via `POST /api/auth/login`
2. Une **clé API principale d’opérateur** (le jeton permanent affiché une seule fois lors de la création du compte opérateur)

Les deux sont acceptés par le même chemin `authenticate_operator` que celui utilisé par la console et la CLI.

### Connexion avec mot de passe

```bash
curl -sS -X POST 'https://exchange.ivjn.us/api/auth/login' \
  -H 'Content-Type: application/json' \
  -d '{"email":"alice@example.com","password":"your-password-here"}'
```

`email` identifie le compte et doit être une adresse valide. Le service la normalise (suppression des espaces en début et fin, conversion en minuscules) et exige sa vérification. Les déploiements existants peuvent encore envoyer `{ "username": "alice", "password": "..." }` jusqu’à la migration de ces comptes vers l’e-mail ; une valeur contenant `@` dans `username` est traitée comme une adresse e-mail. Une adresse invalide est refusée avant la vérification du mot de passe.

Exemple de réponse :

```json
{
  "username": "alice",
  "email": "alice@example.com",
  "tenant_id": "00000000-0000-0000-0000-000000000000",
  "token": "<prefix>.<secret>",
  "expires_in": 2592000
}
```

Utilisez `token` comme identifiant Bearer. Appelez `POST /api/auth/logout` avec le même en-tête pour révoquer cette session. La déconnexion ne révoque pas les clés API principales des opérateurs.

### Clé API d’opérateur (jeton principal)

```bash
TOKEN='<prefix>.<secret>'   # issued at operator creation; store offline
curl -sS 'https://exchange.ivjn.us/api/devices' \
  -H "Authorization: Bearer ${TOKEN}"
```

## Lister les appareils

```bash
curl -sS 'https://exchange.ivjn.us/api/devices' \
  -H "Authorization: Bearer ${TOKEN}"
```

Chaque élément contient `id`, `name`, `online`, `enabled`, `info`, des horodatages et `active_sessions`. L’inventaire des appareils est constitué de métadonnées Exchange. Lister un appareil n’établit pas la session scellée nécessaire pour le piloter.

## Refus des actions HTTP et administration des sessions

Toutes les opérations sur les appareils utilisent des sessions WebSocket scellées. Les routes HTTP ci-dessous ne peuvent pas établir la négociation client/appareil et renvoient donc HTTP `409` pour les requêtes authentifiées, autorisées et validées. Le refus intervient avant toute transmission à l’appareil : aucune commande, demande de capteurs, sonde réseau, mise à jour, invocation ubus, requête de tâche maintenue ou commande prédéfinie n’est envoyée à l’agent. Même un appareil en ligne ne rend pas ces routes exécutables.

L’authentification, les permissions et la validation des entrées restent applicables. Des identifiants invalides entraînent `401` ; une opération interdite entraîne `403` ; les commandes, URL et corps invalides conservent leurs erreurs de validation, notamment `400`. Le `409` des sessions scellées ne remplace pas les échecs de validation ou d’autorisation antérieurs. Les permissions des commandes prédéfinies suivent leur propre politique ; les satisfaire n’autorise toujours pas l’exécution HTTP.

| Méthode et chemin | Comportement après authentification, autorisation et validation |
| --- | --- |
| `POST /api/devices/{id}/exec` | `409` : exécuter dans une session WebSocket scellée |
| `GET /api/devices/{id}/exec/holds` | `409` : consulter les tâches maintenues dans une session WebSocket scellée |
| `POST /api/devices/{id}/exec/holds/{job_id}/kill` | `409` : arrêter les tâches maintenues dans une session WebSocket scellée |
| `GET /api/devices/{id}/exec/holds/{job_id}/cat` | `409` : lire la sortie des tâches maintenues dans une session WebSocket scellée |
| `POST /api/devices/{id}/sensors` | `409` : collecter les capteurs dans une session WebSocket scellée |
| `POST /api/devices/{id}/net` | `409` : exécuter les sondes réseau dans une session WebSocket scellée |
| `POST /api/devices/{id}/update` | `409` : vérifier ou appliquer les mises à jour dans une session WebSocket scellée |
| `POST /api/devices/{id}/ubus` | `409` : invoquer ubus dans une session WebSocket scellée |
| `POST /api/devices/{id}/commands/{cmd}/run` | `409` : récupérer la commande prédéfinie et l’exécuter dans une session WebSocket scellée avec un grant manage autorisé par l’appareil |

Par exemple, cette requête d’action valide vérifie le refus ; elle ne constitue pas une interface d’exécution de commandes :

```bash
curl -i -X POST "https://exchange.ivjn.us/api/devices/${DEVICE_ID}/exec" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{"command":"uname -a","timeout_secs":30}'
```

Attendez HTTP `409` avec le message d’erreur anglais `Sealed sessions are required.` (traduit selon la langue de la requête). Établissez une session WebSocket scellée native pour effectuer l’opération. Cette route HTTP ne renvoie aucune réponse de commande exécutée avec succès.

### Administration des sessions Exchange

L’inventaire et l’annulation des sessions restent disponibles par HTTP, car ils administrent les métadonnées de routage Exchange. Ils n’exécutent pas d’opération sur l’appareil et n’ouvrent pas de session d’appareil supplémentaire.

| Méthode et chemin | Corps en cas de succès |
| --- | --- |
| `GET /api/devices/{id}/sessions` | `{sessions:[DeviceSessionInfo], limit, count}` |
| `POST /api/devices/{id}/sessions/{session_id}/kill` | `{session_id, released}` |

Les champs de `DeviceSessionInfo` sont `session_id` (identifiant numérique côté appareil), `operator_session_id`, `kind`, `hold`, `age_secs` et `deadline_unix_ms`. `limit` vaut `max_sessions_per_device` (32 par défaut) ; `count` est la longueur actuelle de la liste. La liste lit directement l’état local du hub Exchange ; un appareil localement hors ligne entraîne `503`. Kill utilise l’identifiant **côté appareil**, envoie l’annulation appropriée (y compris pour un exec maintenu) et libère la route localement même si l’appareil ne répond pas. Une session absente ou un appareil localement hors ligne renvoie HTTP `200` avec `released:false`. Les opérateurs en lecture seule reçoivent `403` pour les deux points de terminaison.

```bash
curl -sS "https://exchange.ivjn.us/api/devices/${DEVICE_ID}/sessions" \
  -H "Authorization: Bearer ${TOKEN}"

curl -sS -X POST "https://exchange.ivjn.us/api/devices/${DEVICE_ID}/sessions/${SESSION_ID}/kill" \
  -H "Authorization: Bearer ${TOKEN}"
```

Avant de refuser une requête pour saturation des sessions, Exchange récupère les routes d’opérateurs disparus et celles dont l’échéance est dépassée : délai exec + 15 secondes, transfert de fichiers 3600 secondes + 15 secondes, autres types 24 heures. Un exec maintenu dont l’échéance n’est pas dépassée est détaché à la déconnexion de l’opérateur, sans être tué.

### Opérations CLI scellées natives

La CLI établit des sessions scellées dans le cadre des opérations normales sur les appareils. Effectuez d’abord l’approbation de la racine d’espace de travail et l’autorisation de l’appareil ; voir [sessions scellées](/docs/fr/sealed-sessions.md). Il n’existe aucune option de passage en clair. Les commandes normales n’exigent aucun paramètre de scellement supplémentaire :

```bash
remote-cli exec "$DEVICE_ID" -- uname -a
remote-cli shell "$DEVICE_ID"
remote-cli exec-holds "$DEVICE_ID"
remote-cli exec-hold-cat "$DEVICE_ID" "$JOB_ID"
remote-cli exec-hold-kill "$DEVICE_ID" "$JOB_ID"
remote-cli sensors "$DEVICE_ID" --json
remote-cli net "$DEVICE_ID" ping 1.1.1.1
remote-cli net "$DEVICE_ID" http https://example.com/
remote-cli ubus "$DEVICE_ID" system board '{}'
remote-cli commands list "$DEVICE_ID"
remote-cli commands run "$DEVICE_ID" "$COMMAND_ID"
```

Les opérations sur les tâches maintenues s’effectuent dans des sessions scellées ; l’administration des sessions Exchange reste disponible séparément avec `remote-cli sessions` et `remote-cli session-kill`. Récupérer les métadonnées d’une commande prédéfinie n’autorise pas son exécution : l’appareil doit accepter le grant manage de l’opérateur scellé.

## Annuaire des clés de sessions scellées et relais des autorisations

Exchange conserve les clés publiques des opérateurs et les jetons d’autorisation `rsg1.` signés par le propriétaire pour permettre aux clients de se découvrir. **Cela ne constitue pas une autorisation.** Les appareils vérifient toujours la signature du propriétaire selon leur politique locale. Exchange indexe uniquement la charge utile du jeton (`owner_key`, `operator_key`, `device_id`, `level`, `expires_at`) et ne considère jamais un téléversement comme une permission. Exchange ne peut pas falsifier les signatures racines, mais peut retenir des identifiants, présenter une liste trompeuse à un client racine qui approuve automatiquement, observer les métadonnées ou refuser le service.

La racine de l’espace de travail (`rsr1.`) et les invitations d’autorisation (`rsi1.`) utilisent le même type de relais. Exchange vérifie le format du jeton et l’auto-signature Ed25519 (une invitation doit aussi correspondre à la clé racine enregistrée) et ne décide **pas** quelles clés opérateur sont approuvées. `GET /api/seal/roster` fournit au superadministrateur une vue des membres, des rôles et des `operator_device_grants`. Les clients ne doivent pas faire confiance à une nouvelle clé avant son approbation locale par le client racine. Voir `docs/sealed-sessions.md`.

Tout membre authentifié de l’espace de travail peut lister les clés, enregistrer sa propre clé et téléverser des autorisations (celles-ci s’authentifient elles-mêmes). Le `DELETE` d’une clé est réservé au compte qui l’a enregistrée (les administrateurs peuvent toutes les supprimer). Le `DELETE` d’une autorisation est réservé à son auteur (les administrateurs peuvent toutes les supprimer). `GET /api/seal/grants` ignore les lignes expirées.

La preuve d’enregistrement de clé est une signature Ed25519 de :

```text
remote-seal-key-register:v1:<workspace_id>:<operator_id>:<public_key>:<issued_at_ms>
```

`issued_at_ms` doit se situer à ±10 minutes de l’horloge du serveur. `public_key` et `proof` sont en base64 standard (clé de 32 octets, signature de 64 octets).

| Méthode et chemin | Corps | Succès |
| --- | --- | --- |
| `POST /api/seal/keys` | `{"public_key","label"?,"proof","issued_at_ms"}` | clé enregistrée (insertion ou mise à jour pour le même compte) |
| `GET /api/seal/keys` | — | `[{operator_id, username, email, public_key, label, created_at, last_seen_at}]` |
| `DELETE /api/seal/keys/{key}` | — | `204` (`{key}` est la clé publique encodée pour une URL) |
| `POST /api/seal/grants` | `{"tokens":["rsg1.…"]}` | `{stored:[…], errors:[…]}` |
| `GET /api/seal/grants?mine=1` | — | autorisations dont `operator_key` est l’une des clés enregistrées de l’appelant. Chacune dont `owner_key` n’est pas la racine publiée inclut aussi `chain` (jetons de certificats, éventuellement vide) |
| `GET /api/seal/grants?owner_key=…` | — | autorisations signées par cette clé de propriétaire |
| `DELETE /api/seal/grants/{id}` | — | `204` |
| `PUT /api/seal/root` | `{"token":"rsr1.…","rotation":"rsx1.…"?}` | racine d’espace de travail enregistrée. Superadministrateur uniquement. Le format et la signature propre de la clé sont vérifiés ; ce n’est pas une approbation. `rotation` (facultatif) doit être signé par la racine actuellement publiée et désigner la nouvelle racine ; Exchange pousse alors `seal_root {token, rotation}` vers les appareils scellés en ligne, qui ne changent leur ancrage que si la rotation est signée par la racine déjà approuvée. Sans rotation valide, les appareils ancrés à l’ancienne racine la conservent |
| `GET /api/seal/root` | — | `{workspace_id, root_key, payload, signature, token, created_at}`, ou `404` si aucune racine n’est publiée |
| `POST /api/seal/invitations` | `{"token":"rsi1.…"}` | invitation enregistrée. Superadministrateur uniquement, et seulement après la création d’une racine |
| `GET /api/seal/invitations` | — | invitations non expirées, de la plus récente à la plus ancienne (`[]` en leur absence) |
| `GET /api/seal/roster` | — | `{members:[{operator_id, username, email, role, restricted, enabled, keys:[{public_key, label}], device_grants:[{device_id, can_manage}]}]}`. Superadministrateur uniquement. Inclut les membres désactivés |

L’indisponibilité de l’annuaire ne contourne ni l’obligation de session scellée ni celle d’autorisation sur l’appareil. `GET /api/seal/root` renvoie `404` si aucune racine d’espace n’a été publiée.

```bash
# Register this operator key (proof signed locally)
curl -sS -X POST "$BASE/api/seal/keys" \
  -H "Authorization: Bearer ${TOKEN}" -H 'Content-Type: application/json' \
  -d '{"public_key":"<ed25519-b64>","label":"cli","proof":"<sig-b64>","issued_at_ms":1700000000000}'

curl -sS "$BASE/api/seal/keys" -H "Authorization: Bearer ${TOKEN}"

curl -sS -X POST "$BASE/api/seal/grants" \
  -H "Authorization: Bearer ${TOKEN}" -H 'Content-Type: application/json' \
  -d '{"tokens":["rsg1.…"]}'

curl -sS "$BASE/api/seal/grants?mine=1" -H "Authorization: Bearer ${TOKEN}"
```

### Certificats intermédiaires (`rsc1.`)

Exchange conserve les certificats intermédiaires pour qu’une clé non racine puisse signer des autorisations acceptées par les appareils. Il **vérifie** la signature Ed25519 de l’émetteur et **ne peut pas falsifier** de certificat. Téléverser un jeton ne constitue pas une autorisation : l’appelant doit être l’opérateur de l’émetteur. Seul le superadministrateur de l’espace peut publier un certificat signé par la racine (`issuer_key` égal à la racine publiée de l’espace). Seul le compte dont la clé de scellement enregistrée est `issuer_key` peut publier un intermédiaire ; cet émetteur doit lui-même être le sujet d’un certificat enregistré, non révoqué, non expiré et doté de `can_issue`. Le certificat enfant doit respecter le niveau, la portée d’appareils et l’expiration du parent (mêmes vérifications que sur les appareils). Republier le même couple `(issuer_key, subject_key)` remplace l’ancienne ligne et annule la révocation.

`GET /api/seal/certs/chain?subject_key=` renvoie `{chain:[tokens]}` en remontant les liens d’émetteurs jusqu’à la racine publiée, avec au plus 4 certificats, celui émis par la racine en premier, ou `404`. Tout membre authentifié peut l’appeler. `GET /api/seal/grants?mine=1` ajoute cette `chain` aux autorisations dont le signataire n’est pas la racine, afin que les clients la reçoivent avec l’autorisation. Les autorisations signées par la racine omettent `chain`.

`GET /api/seal/certs` liste toutes les lignes, y compris celles révoquées (administrateurs et superadministrateur). `DELETE /api/seal/certs/{serial}` (compte de l’émetteur ou superadministrateur) définit `revoked`. Cela arrête seulement la distribution. Les appareils continuent d’honorer une copie déjà reçue jusqu’à son expiration ; la révocation effective relève de `revoked_keys` dans la politique de l’appareil. `{serial}` est le numéro de série en base64 standard encodé pour une URL.

| Méthode et chemin | Corps | Succès |
| --- | --- | --- |
| `POST /api/seal/certs` | `{"token":"rsc1.…"}` | certificat enregistré (insertion ou mise à jour par émetteur et sujet) |
| `GET /api/seal/certs/chain?subject_key=` | — | `{chain:["rsc1.…"]}`, celui émis par la racine en premier, ou `404` |
| `GET /api/seal/certs` | — | `[{serial, issuer_key, subject_key, token, expires_at_ms, created_at, revoked}]`. Administrateur ou superadministrateur |
| `DELETE /api/seal/certs/{serial}` | — | `204`. Émetteur ou superadministrateur. Arrête seulement la distribution |

```bash
curl -sS -X POST "$BASE/api/seal/certs" \
  -H "Authorization: Bearer ${TOKEN}" -H 'Content-Type: application/json' \
  -d '{"token":"rsc1.…"}'

curl -sS "$BASE/api/seal/certs/chain?subject_key=<ed25519-b64>" \
  -H "Authorization: Bearer ${TOKEN}"
```

### Sauvegarde de la clé racine d’espace de travail (facultative)

`PUT` / `GET` / `DELETE /api/seal/backup` conservent un blob opaque `rsb1.` par compte. `PUT` et `GET` sont réservés au superadministrateur : le blob contient la clé privée **racine** de l’espace de travail, chiffrée côté client avec Argon2id (64 Mio, t=3, p=1) et ChaCha20-Poly1305. La phrase secrète est facultative et aucune robustesse n’est imposée. **Avec une phrase secrète vide ou faible, quiconque possède le blob (y compris Exchange) peut déchiffrer la clé racine et contrôler tous les appareils.** Le téléversement est facultatif ; les clients peuvent exporter la clé racine (`rsk1.`) et la conserver hors ligne. Exchange vérifie le préfixe `rsb1.` de `blob` et sa longueur, sans jamais le déchiffrer. `GET` est limité à 5 requêtes par compte, par heure et par processus (HTTP 429). L’audit des lectures, écritures et suppressions ne consigne que le nombre d’octets. Les réponses envoient `Cache-Control: no-store`.

| Méthode et chemin | Corps | Succès |
| --- | --- | --- |
| `PUT /api/seal/backup` | `{"blob":"rsb1.…"}` | `{blob, updated_at}` |
| `GET /api/seal/backup` | — | `{blob, updated_at}`, ou `404` si rien n’est enregistré. Débit limité |
| `DELETE /api/seal/backup` | — | `204`, y compris en l’absence de ligne |

```bash
curl -sS -X PUT "$BASE/api/seal/backup" \
  -H "Authorization: Bearer ${TOKEN}" -H 'Content-Type: application/json' \
  -d '{"blob":"rsb1.…"}'

curl -sS "$BASE/api/seal/backup" -H "Authorization: Bearer ${TOKEN}"

curl -sS -X DELETE "$BASE/api/seal/backup" -H "Authorization: Bearer ${TOKEN}"
```

## Authentification de l’extension de navigateur par chaîne racine

Les opérations du navigateur utilisent des sessions WebSocket scellées natives, plutôt qu’une exécution HTTP ponctuelle. L’extension génère elle-même une clé opérateur Ed25519. Avec un jeton Bearer opérateur, elle obtient `tenant_id` / `operator_id` auprès de `/api/auth/me`, puis appelle `POST /api/seal/keys` avec `{public_key, label, proof, issued_at_ms}`. La preuve de possession (PoP) signe :

```text
remote-seal-key-register:v1:<workspace_id>:<operator_id>:<public_key>:<issued_at_ms>
```

La clé et la signature sont en base64 standard (32/64 octets) ; l’horodatage doit se situer à ±10 minutes de l’horloge serveur. L’enregistrement prouve la possession, pas l’autorisation. L’extension synchronise `GET /api/seal/root`, `GET /api/seal/grants?mine=1` et les jetons de certificats inclus avec les autorisations ou récupérés via `GET /api/seal/certs/chain?subject_key=<url-encoded-key>`. Les autorisations doivent être vérifiables jusqu’à la racine de confiance de l’espace, directement ou par une chaîne de certificats valide, avec la portée d’appareils, le niveau et la période de validité corrects. L’enregistrement par jeton d’appareil reste distinct de ce parcours d’annuaire par jeton opérateur.

L’extension vérifie les messages hello natifs entrants à travers sa racine et inclut son propre hello signé, son autorisation et sa chaîne dans `SealAccept.peer`. La CLI et la console vérifient cette preuve et exigent `peer.operator_key == accept.device_key` avant d’accepter la réponse signée. Les agents Rust utilisent leur politique racine habituelle lorsque l’extension agit comme opérateur. Les preuves invalides ou absentes entraînent un refus ; il n’existe ni magasin de confiance/interface propre au navigateur ni repli hérité. Le TOFU des appareils natifs et de la racine d’espace demeure ; l’extension conserve la première racine par serveur/espace et refuse les changements.

**Sécurité :** vérifiez la première racine par un canal indépendant ; la PoP ou un rôle serveur ne vaut pas approbation par la racine. Examinez les clés inconnues avant l’approbation automatique par le client racine. Protégez les profils/clés des terminaux et les scripts hébergés. Supprimer une entrée d’annuaire ne révoque pas les identifiants signés en cache. Exchange peut voir les métadonnées, retenir les identifiants ou refuser le service ; le scellement n’isole pas un opérateur hostile disposant de l’accès CDP complet.

## Choisir WebSocket ou HTTPS

| Besoin | Solution |
| --- | --- |
| Commandes, consultation ou annulation de tâches maintenues, capteurs, sondes réseau, mises à jour, ubus, commandes prédéfinies | Session WebSocket scellée native |
| PTY interactif, sortie en continu, transfert de fichiers, écran/arbre/appui Android | Session WebSocket scellée native |
| Authentification, inventaire des appareils, annuaire des clés/autorisations/certificats, sauvegarde racine | API HTTPS |
| Inventaire et annulation des sessions de routage Exchange | Points de terminaison HTTPS d’administration des sessions |
| Présence des appareils en direct et connexion opérateur persistante | WebSocket ; les opérations sur les appareils exigent toujours le scellement |

Parcours de ticket navigateur (les identifiants ne figurent jamais dans l’URL WebSocket) :

1. `POST /api/ws-ticket` avec `Authorization: Bearer …`.
2. Connexion à `GET /ws/browser` et envoi de la première trame JSON `{"ticket":"…"}`.
3. Établissement de la négociation scellée native client/appareil avant toute opération. Le grant de l’opérateur et sa chaîne de certificats doivent être validés jusqu’à la racine de confiance de l’appareil.

Les clients natifs utilisent `GET /ws/operator` avec authentification opérateur et la même obligation de session scellée. Une connexion WebSocket ou une connexion de compte réussie ne suffit pas à autoriser une opération sur l’appareil. Voir [Console Web et Web Push](/docs/fr/web-console-and-notifications.md), [protocole](/docs/fr/websocket-protocol.md) et [sessions scellées](/docs/fr/sealed-sessions.md).

## Aide-mémoire des statuts d’erreur

| Statut | Cause habituelle |
| --- | --- |
| `400` | Entrée d’action invalide, par exemple commande vide ou URL source de mise à jour invalide ; la validation précède le refus pour obligation de session scellée |
| `401` | Jeton Bearer absent ou invalide |
| `403` | Les permissions du compte ou de l’appareil n’autorisent pas la requête |
| `409` | Une action HTTP valide et autorisée exige une session WebSocket scellée ; aucune transmission à l’appareil |
| `404` | Entrée d’annuaire absente ou racine d’espace non publiée, selon le contrat du point de terminaison |
| `429` | Limitation propre au point de terminaison, notamment les lectures de sauvegarde racine |
| `503` | L’inventaire des sessions ne trouve pas l’appareil dans l’état local du hub Exchange |

Les routes HTTP d’action sur les appareils n’attendent aucune sortie de l’agent ; leur résultat normal après autorisation est donc `409`, et non un succès d’exécution, une erreur de transmission hors ligne ou une expiration d’exécution HTTP. Les erreurs des API de métadonnées, d’authentification et d’annuaire conservent leur sens propre à chaque point de terminaison.

## Plan de validation manuelle

Utilisez un compte et un appareil de test dont les permissions sont connues. Les vérifications de refus HTTP n’exigent aucune exécution sur un appareil :

1. Authentifiez-vous et listez les appareils par HTTPS ; vérifiez les permissions du compte et de l’appareil.
2. Soumettez une requête valide à chaque route d’action du tableau de refus. Attendez HTTP `409` et confirmez qu’aucune action ne parvient à l’agent, y compris les lectures de tâches maintenues et les exécutions de commandes prédéfinies.
3. Vérifiez séparément les identifiants invalides, les comptes interdits et les entrées invalides. Attendez les erreurs d’authentification, de permission et de validation correspondantes, sans supposer que toute requête renvoie `409`.
4. Consultez l’inventaire des sessions Exchange. Pour une session jetable explicitement sélectionnée, testez l’annulation et vérifiez `released` ; une session absente renvoie `released:false`. La liste des sessions d’un appareil localement hors ligne renvoie `503`.
5. Finalisez l’approbation de la racine et les autorisations, puis exécutez une opération CLI native inoffensive telle que `remote-cli exec "$DEVICE_ID" -- uname -a`. Vérifiez son succès via la négociation scellée. Un grant d’appareil absent ou invalide doit entraîner un refus.
6. Validez indépendamment l’enregistrement de clé, la récupération des autorisations/certificats, la sauvegarde racine facultative et l’authentification de l’extension par chaîne racine ; ces API HTTPS restent disponibles selon les contrats ci-dessus.

## Destinations d’export d’audit

Disponible uniquement sur Exchange PostgreSQL, isolé par espace authentifié. Administrateurs et superadministrateurs seulement ; les autres comptes reçoivent 403. Non pris en charge par exchange-worker ; pas d’interface de configuration Console Web/mobile.

```text
GET    /api/audit/sinks
POST   /api/audit/sinks
PATCH  /api/audit/sinks/{id}
DELETE /api/audit/sinks/{id}
POST   /api/audit/sinks/{id}/test
GET    /api/audit/sinks/status
```

```sh
printf '%s' '{"name":"archive","kind":"webhook","endpoint":"https://audit.example.net/events","secret":"REPLACE-WITH-RANDOM-SECRET","mode":"full"}' | remote-cli audit sink add
remote-cli audit sink list
printf '%s' '{"interval_ms":500,"batch_size":16}' | remote-cli audit sink update 1
remote-cli audit sink test 1
remote-cli audit sink status
remote-cli audit sink remove 1
```

Par défaut, throttled transmet les enregistrements stockés : au-delà de 256 KiB, le texte conserve 250 KiB au début et 6 KiB à la fin ; fichiers sous forme de hash ; flux après la fin de session. full capture avant troncature, découpe JSON original et données binaires (1 MiB par défaut), avec identifiants opération/objet, séquence, décalage, indicateur final, SHA-256 et taille totale. Pour sealed, les deux modes transmettent uniquement métadonnées et références/hashes du chiffré, sans retrouver le clair des terminaux.

Secrets et en-têtes sont stockés chiffrés et affichés comme ***. PATCH conserve *** ; null supprime un en-tête. Files FIFO persistantes asynchrones et reprises exponentielles. Une file throttled pleine supprime et compte les plus anciens ; une file full sur disque exerce une contre-pression. Status indique succès/échecs, attente, dropped et gaps. La suppression élimine les envois en attente et audite leur nombre ; les changements sont audités dans la même transaction.

Les webhooks utilisent un tableau JSON events avec octets base64. Vérifier X-Remote-Timestamp et X-Remote-Signature: sha256=<hex> via HMAC-SHA256(timestamp + "." + corps brut). OTel utilise OTLP/HTTP JSON /v1/logs, body.bytesValue pour les blocs, remote.device.id / remote.operator / remote.action et service.name configurable. gRPC nécessite un Collector, sans export direct.

Exemples de configuration par défaut, charge webhook, vérification et Collector ci-dessous. Vérifier le corps HTTP brut ; persister et dédupliquer par delivery_id avant de retourner 2xx. Activer allow_structured_metadata dans Loki 3.x, ajouter une source Loki dans Grafana et interroger {service_name="remote-exchange"}. Pour gRPC, remplacer pipeline exporters par [otlp/grpc]. Utiliser TLS et authentification en production.

```json
{"name":"collector","kind":"otel","mode":"throttled","endpoint":"https://collector.example.net:4318/v1/logs","headers":{"Authorization":"Bearer REPLACE"},"service_name":"remote-exchange","resource":{"deployment.environment.name":"production"},"batch_size":32,"interval_ms":1000,"chunk_bytes":1048576,"queue_limit":10000,"overflow_bytes":10737418240,"actions":[],"devices":[],"operators":[],"enabled":true,"ca_pem":null}
```

```json
{"version":1,"events":[{"delivery_id":123,"type":"full","mode":"full","entry":{"id":42,"action":"exec","operator_name":"alice","device_id":"dev"},"data":{"kind":"binary","asset_id":"asset-uuid","path":"/data_base64","sequence":0,"offset":0,"last":true,"data_base64":"YWJj","total_size":3,"sha256":"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad","context":{"direction":"output","frame_sequence":0,"stream_offset":0}}}]}
```

```python
import hashlib, hmac, time

def verify(headers, raw_body, secret):
    stamp = headers['X-Remote-Timestamp']
    if abs(time.time() - int(stamp)) > 300:
        raise ValueError('expired webhook')
    expected = 'sha256=' + hmac.new(
        secret.encode(), stamp.encode() + b'.' + raw_body, hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(expected, headers['X-Remote-Signature']):
        raise ValueError('invalid signature')
```

```yaml
receivers:
  otlp:
    protocols:
      http:
        endpoint: 127.0.0.1:4318
processors:
  batch: {}
exporters:
  otlphttp/loki:
    endpoint: http://loki:3100/otlp
  otlp/grpc:
    endpoint: telemetry.example.net:4317
    headers:
      Authorization: "${env:OTEL_AUTHORIZATION}"
    tls:
      insecure: false
service:
  pipelines:
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlphttp/loki]
```

| Champ d’audit | Champ OTLP |
|---|---|
| service_name / resource | Resource service.name / attributes |
| entry.started_at | timeUnixNano |
| entry.id / delivery_id | remote.audit.id / remote.delivery.id |
| entry.device_id / operator_name / action | remote.device.id / remote.operator / remote.action |
| entry.tenant_id | remote.workspace.id |
| entry.started_at / ended_at / detail | remote.started_at / remote.ended_at / remote.result |
| traceId / spanId | traceId / spanId |
| data.data_base64 | body.bytesValue / remote.chunk.* |
| stored events | body.stringValue |
