# Appareils Linux, FreeBSD, NetBSD, Windows, macOS et Android

> HTML: https://remote.ivjn.us/docs/fr/platforms/

Les appareils Linux (glibc / musl), FreeBSD, NetBSD, Windows, macOS et Android partagent `remote-agent`, Exchange, `remote-cli` et le client mobile d'administration. L'agent et le CLI utilisent des noms génériques ; les variables d'environnement sont unifiées en `REMOTE_*`.

## Installation en une commande (Windows / Linux / macOS / FreeBSD / NetBSD)

Le script officiel télécharge et vérifie le paquet adapté au système, place les binaires dans `~/.local/bin`, puis appelle `remote-agent install` pour enregistrer le démarrage de l'Agent à la connexion :

Choisissez la commande adaptée et copiez chaque commande séparément.

**Installer Agent sur l’appareil géré**

```sh
curl -fsSL https://remote.ivjn.us/install.sh | bash -s -- agent
```

**Installer CLI sur le poste de commande**

```sh
curl -fsSL https://remote.ivjn.us/install.sh | bash -s -- cli
```

Sous Windows, utilisez Git Bash. Pour l'Agent, placez les identifiants après le tube : `bash -s -- agent --device-id NAME --token TOKEN` (ou utilisez un `config.json` / `REMOTE_CONFIG` existant). Les variables placées avant `curl` ne sont pas transmises à bash. Pour systemd au niveau système sous Linux, ajoutez `--system`. Le script distingue automatiquement glibc et musl (Alpine, etc.) et prend aussi en charge Linux arm64 (aarch64) ; `REMOTE_LIBC=musl` force le paquet musl. FreeBSD / NetBSD : les installations utilisateur restent dans `~/.local/bin` ; `sudo remote-agent install --system --config CONFIG` enregistre rc.d. Le contrôle du bureau nécessite une session X11 avec `python3` et `xdotool`. Android et OpenWrt utilisent leurs paquets dédiés.

## Commandes et services

Les commandes de l’appareil et de la CLI sont `remote-agent` et `remote-cli`. `remote-cli` écrit par défaut dans `remote-cli/config.toml` ; une valeur explicite de `REMOTE_CLI_CONFIG` est prioritaire.

Le paquet OpenWrt, le fichier UCI et le service init portent le nom `remote-agent`. Le fichier d’état est `/var/run/remote-agent.state` et la configuration se trouve dans `/etc/config/remote-agent`. Le module LuCI `luci-app-remote` dépend de ce paquet. `REMOTE_AGENT_SRC` définit le chemin des sources de compilation.

| Appareil | Exécution de commandes | Terminal interactif | Exécution permanente | ubus |
|---|---|---|---|---|
| Linux | `/bin/sh -c` | Unix PTY | systemd ou processus au premier plan | OpenWrt uniquement |
| Linux musl (Alpine 3.x, etc.), x64 / arm64 | `/bin/sh -c` | Unix PTY | Processus au premier plan ou service OpenRC personnalisé (sans systemd) | Non pris en charge |
| FreeBSD 14+, amd64 / arm64 | `/bin/sh -c` | Unix PTY | rc.d (root/système) ; premier plan (utilisateur) | Non pris en charge |
| NetBSD 10+, amd64 | `/bin/sh -c` | Unix PTY | rc.d (root/système) ; premier plan (utilisateur) | Non pris en charge |
| Windows 10 1809+/11, Server 2019+ | Windows PowerShell 5.1 | ConPTY + PowerShell | Tâche planifiée à la connexion ou processus au premier plan | Non pris en charge |
| macOS 11+, Apple Silicon / Intel | `/bin/sh -c` | Unix PTY | LaunchAgent launchd ou processus au premier plan | Non pris en charge |
| Android 8.0+, arm64 / x86_64 | `/system/bin/sh -c` | Unix PTY | Service au premier plan de l'APK autonome | Non pris en charge |

## Configuration commune

Enregistrez d'abord l'appareil avec `remote-cli device add <id>` pour obtenir son jeton. Sous Linux/Windows, créez un `config.json` lisible uniquement par le compte qui l'exécute :

```json
{
  "server": "wss://mgmt.example.com",
  "device_id": "my-device",
  "token": "REPLACE_WITH_DEVICE_TOKEN"
}
```



```sh
remote-agent --config /path/to/config.json
```

La priorité va à la ligne de commande / aux variables `REMOTE_SERVER`, `REMOTE_DEVICE_ID`, `REMOTE_TOKEN`, puis au JSON et, uniquement sous OpenWrt, à UCI en dernier recours. Le JSON peut aussi définir `state_file` ; `--state-file` / `REMOTE_STATE_FILE` le remplace. Sans fichier d'état explicite, OpenWrt utilise toujours `/var/run/remote-agent.state` ; les autres plateformes utilisent `remote-agent/agent.state` sous `LOCALAPPDATA`, `XDG_STATE_HOME`, puis `$HOME/.local/state`, dans cet ordre, ou le répertoire temporaire sans répertoire utilisateur. Chaque instance doit avoir son propre chemin d'état.

Utilisez `wss://` en production. Le `ws://127.0.0.1` des exemples sert uniquement à la validation locale.

### Notifications de connexion de l'Agent

Définissez `"notify_urls": ["https://example.invalid/notify"]` dans le JSON, ou utilisez `remote-agent --notify-url https://example.invalid/notify` (répétable) ; `REMOTE_NOTIFY_URL` accepte des adresses séparées par des virgules. Une liste non vide de la ligne de commande / de l'environnement remplace le JSON ; sinon le JSON est utilisé, puis, uniquement sous OpenWrt, la valeur UCI `remote-agent.main.notify_url` séparée par des virgules. Si toutes les sources sont absentes ou vides, aucun POST n'est envoyé.

Un enregistrement réussi envoie `agent.connected`, la fin d'une session établie envoie une fois `agent.disconnected`, et un refus explicite envoie uniquement `agent.rejected` ; les échecs réseau avant l'enregistrement ne déclenchent aucune notification. Le POST reprend l'objet JSON d'Exchange Web Push (`title`, `body`, `url`, `event`, `device_id`, `occurred_at`), mais décrit la connexion de l'agent plutôt que la présence de l'appareil : `event` commence par `agent.` au lieu de `device.`, `url` vaut `""` (aucun chemin de console Web Push) et `tag` est absent. Le corps des notifications de déconnexion et de refus contient un bref motif sûr, sans jeton d'appareil ; l'horodatage est au format RFC3339 UTC.

Les notifications sont envoyées en arrière-plan. Un échec ne journalise que le nom de l'événement, jamais l'adresse de destination, et ne bloque pas la reconnexion. Comme pour Exchange Web Push, il y a au maximum 5 tentatives : les erreurs réseau et HTTP 408/429/5xx sont réessayées ; 404/410 et les autres 4xx non réessayables sont abandonnées. `Retry-After` en secondes entières est prioritaire ; sinon le délai exponentiel commence à 5 secondes, avec une limite de 1 à 900 secondes. Seuls HTTP/HTTPS sont acceptés, sans redirections ; ne placez aucun identifiant, mot de passe ou autre secret dans l'URL. Les adresses contenant un nom d'utilisateur ou mot de passe sont refusées.

### Proxy sortant et maintien de la connexion

Les modes sans interface et `--ui` utilisent le même `client::run` pour joindre Exchange. Le WebSocket lit les variables de proxy héritées par le processus agent : première valeur non vide parmi `HTTPS_PROXY` / `https_proxy`, `ALL_PROXY` / `all_proxy`, puis `HTTP_PROXY` / `http_proxy`. Les hôtes correspondant à `NO_PROXY` / `no_proxy` contournent le proxy. Le WebSocket de `remote-cli` suit les mêmes règles (#36/#37, #39). Pour un démarrage à la connexion ou un service en arrière-plan, définissez ces variables dans son environnement d'exécution ; les définir dans un autre terminal ne les transmet pas à un processus déjà lancé.

Pour le WSS d'un appareil via proxy, un tunnel **HTTP CONNECT** est établi avant la connexion rustls à la cible ; l'ALPN du TLS ClientHello **annonce uniquement `http/1.1`**, comme le CLI. Négocier `h2` casse le chemin actuel de mise à niveau WebSocket HTTP/1.1. Si la connexion reste bloquée avec un proxy tel que Clash, vérifiez sa prise en charge de CONNECT et utilisez une compilation contenant les correctifs #39/#42. Les chemins WebSocket de l'appareil et du CLI ne prennent pas en charge SOCKS ; utilisez un proxy HTTP CONNECT (par exemple `HTTPS_PROXY=http://127.0.0.1:7890`).

Après une connexion directe ou CONNECT, l'agent active `TCP_NODELAY` et le keepalive TCP après 30 secondes d'inactivité. Chaque battement JSON envoie aussi un Ping WebSocket ; à réception d'un Ping, il vide explicitement le Pong automatiquement mis en file. La connexion et l'attente de la réponse d'enregistrement ont chacune une limite de 45 secondes ; les envois et la vidange des Pong après enregistrement sont limités à 30 secondes. Voir [Maintien de connexion et délais](/docs/fr/websocket-protocol.md#maintien-de-connexion-et-délais) (#42).

Les compilations Windows avec `desktop-ui` utilisent un runtime Tokio multithread pour exécuter axum, la zone de notification et le WebSocket de l’appareil en parallèle, même sans interface. Sur les autres plateformes avec cette fonctionnalité, l’interface locale (`--ui` explicite ou lancement sans arguments) utilise aussi plusieurs threads. Les compilations sans cette fonctionnalité et les lancements sans interface avec une configuration explicite sur les autres plateformes utilisent `current_thread`, adapté aux petits appareils.

## Interface de configuration Linux / Windows / macOS

Les compilations activent `desktop-ui` par défaut. Les versions de bureau doivent inclure l’interface locale de configuration. Utilisez `--no-default-features` uniquement pour les serveurs sans interface ou les systèmes embarqués. Pendant la connexion, les champs du serveur de gestion, de l’identifiant et du jeton sont masqués. Ils réapparaissent après l’arrêt de l’accès distant. L’état, les paramètres LAN et le démarrage à la connexion restent visibles.

```sh
cargo build --locked -p agent --release
# Linux / macOS
./target/release/remote-agent --ui
```



```powershell
# Windows
.\target\release\remote-agent.exe --ui
```

Sous Linux / Windows / macOS, un lancement sans paramètres de connexion (par exemple un double-clic sur `remote-agent.exe`) avec `desktop-ui` crée un `config.json` par défaut si aucune configuration utilisable n'existe (seul l'Exchange par défaut est prérempli, device_id/token restent vides) et ouvre la configuration dans le navigateur. Avec une configuration existante, Windows utilise l'icône de notification sans forcer le navigateur, et Linux/macOS affichent l'URL locale (macOS dispose aussi d’une icône dans la barre des menus). Un lancement interactif sans paramètres ni `desktop-ui` crée la configuration par défaut, affiche son chemin et se termine, plutôt que d'afficher seulement `Missing device_id`. OpenWrt / systemd / les tâches planifiées doivent toujours utiliser `--config`, UCI ou `REMOTE_*`.

Un lancement sans arguments et `--ui` explicite suivent la même règle : le navigateur par défaut ne s’ouvre que si la configuration ne contient pas de `device_id`/`token` non vide. Un Agent configuré se connecte en arrière-plan sans rouvrir le navigateur à la connexion utilisateur. Windows conserve l’entrée de la zone de notification pour ouvrir la page ; Linux/macOS utilisent l’URL locale du terminal. `--ui --no-open` empêche toujours l’ouverture automatique, même à la première configuration. `--ui-port 8765` fixe le port ; sinon un port libre est choisi aléatoirement. L’interface écoute uniquement sur `127.0.0.1`. Les ressources sont intégrées, sans polices ni CDN externes.

La zone de notification Windows et la barre des menus macOS donnent accès à la configuration, aux réglages de l’écran de veille, à la gestion des tâches et aux actions du presse-papiers. La gestion des tâches ouvre directement le panneau local. Linux utilise la page de configuration du navigateur.

Obtenez d'abord l'ID et le jeton avec `remote-cli device add <id>`, puis renseignez le serveur, l'ID et le jeton dans **Connection settings** :

- **Save only** : enregistre sans se connecter.
- **Save and connect** : enregistre et connecte ; modifier la configuration en cours d'exécution ferme l'ancienne connexion et en ouvre une avec les nouveaux paramètres.
- **Stop remote access** : déconnecte et annule les exécutions et sessions de terminal en cours ; la configuration est conservée.
- La barre d'état affiche **Connected** après un enregistrement réussi, ou la connexion, la reconnexion ou l'arrêt, ainsi que la dernière erreur de connexion.

Au début de la connexion, l'interface écrit **Connecting** dans son propre fichier d'état avant de lancer `client::run`, pour éviter de conserver la valeur par défaut affichée lorsque le fichier n'existe pas encore (#42).

Le jeton enregistré n'est jamais renvoyé à la page ; un champ vide conserve le jeton existant. La configuration est remplacée atomiquement via un fichier temporaire, avec les permissions `0600` sous Linux et une ACL limitée à l'utilisateur actuel sous Windows. Un JSON endommagé ou illisible déclenche une demande de réparation sans écraser le fichier. Un seul processus d'interface peut ouvrir une même configuration.


Le chemin par défaut est `$XDG_CONFIG_HOME/remote-agent/config.json` sous Linux/macOS (`~/.config/remote-agent/config.json` si absent), et `%LOCALAPPDATA%\remote-agent\config.json` sous Windows. `--ui --config <path>` ou `REMOTE_CONFIG` désigne un JSON existant. L'interface refuse les remplacements `--server`, `--device-id`, `--token`, `--state-file` et leurs variables d'environnement pour éviter un écart entre valeurs enregistrées et connexion réelle. Le JSON `state_file` s’applique aussi à l’UI ; sinon, elle utilise le même chemin persistant que headless pour conserver l’identité scellée et la confiance.

La page propose **Start at login / Démarrer à la connexion** (`autostart: { supported, enabled, detail }` dans `GET /api/status`, et `{ "enabled": true|false }` envoyé à `POST /api/autostart`). Aucune élévation n'est nécessaire :

- **macOS** : installe/supprime `~/Library/LaunchAgents/us.ivjn.remote-agent.plist` (même modèle que `install/macos/install.sh`), avec `launchctl bootstrap` / `bootout` ; le binaire est de préférence copié dans `~/.local/bin/remote-agent` et la configuration dans `~/.config/remote-agent/config.json` (conservée si elle existe).
- **Windows** : enregistre/supprime la tâche `RemoteAgent` de l'utilisateur actuel (AtLogOn), selon `install/windows/install.ps1`, dans `%LOCALAPPDATA%\remote-agent`.
- **Linux** : installe/supprime l'unité systemd utilisateur (`install/linux/remote-agent.service`), avec `systemctl --user enable --now` / `disable`.

L'activation copie si nécessaire l'exécutable actuel vers le chemin prévu en conservant le `config.json` existant ; réponses et journaux n'affichent pas le jeton. L'interface gère toujours le processus actuel ; après activation du démarrage automatique et fermeture de l'interface, LaunchAgent / la tâche planifiée / systemd lancera l'agent en arrière-plan à la prochaine connexion.

**Fermer l’onglet ne stoppe pas l’Agent ; quitter son processus coupe la connexion. Un appareil configuré se reconnecte au lancement de l’interface ; la première configuration nécessite Save and connect.** L’interface gère le processus actuel et permet d’activer ou de désactiver le démarrage à la connexion. Arrêtez le service existant avant de lancer une autre interface pour le même appareil. Après configuration et activation du démarrage automatique, vous pouvez fermer l’onglet. Les opérations utilisent les permissions du compte qui exécute l’Agent.

L'URL locale contient une clé d'accès aléatoire propre à cette interface : ne la partagez pas. L'API vérifie aussi Host et Origin pour refuser les pages externes. Le jeton est stocké sans chiffrement dans la configuration ; l'utilisateur local actuel et les administrateurs peuvent le lire. Les compilations allégées sans `desktop-ui` conservent la ligne de commande ; OpenWrt et Android utilisent leurs interfaces existantes.

## FreeBSD

Le site officiel fournit `remote-freebsd-x64.tar.gz` et `remote-freebsd-arm64.tar.gz` (compilés sous Linux avec `cargo zigbuild`, liés à la libc FreeBSD 14). Après extraction :

```sh
mkdir -p ~/.local/bin
cp remote-agent remote-cli ~/.local/bin/
sudo remote-agent install --system --device-id NAME --token TOKEN
```

Le code actuel prend en charge rc.d, les métriques sysctl, les compteurs réseau `netstat -ibn`, les températures/ventilateurs FreeBSD et `envstat` NetBSD. Le contrôle X11 est annoncé si `DISPLAY`, `python3` et `xdotool` sont disponibles ; les captures nécessitent aussi Pillow. Médias et paramètres système restent indisponibles. La mise à jour reconnaît les archives BSD x64/arm64 si le manifeste contient un paquet vérifié correspondant. De nouveaux paquets et des tests natifs BSD restent nécessaires.

## NetBSD

Le site officiel fournit `remote-netbsd-x64.tar.gz` (amd64, compilation croisée sous Linux avec zig et la libc NetBSD 10 ; seules dépendances dynamiques : libc / libpthread / libutil). Utilisation identique à FreeBSD :

```sh
mkdir -p ~/.local/bin
cp remote-agent remote-cli ~/.local/bin/
sudo remote-agent install --system --device-id NAME --token TOKEN
```

Fonctionnalités : connexion à Exchange, `exec` / PTY / transfert de fichiers ; métriques via sysctl (`kern.cp_time`, `hw.physmem64`, `vm.uvmexp2`) ; le contrôle graphique du bureau est une implémentation factice non annoncée. La plateforme signalée est `netbsd`. La mise à jour automatique et le redémarrage rc.d fonctionnent comme sous FreeBSD (`remote-netbsd-x64.tar.gz` / `remote-netbsd-arm64.tar.gz`), si le manifeste contient l'archive.

## Paquets officiels OpenWrt 25.12

Le site officiel fournit `remote-agent` empaqueté avec le SDK officiel OpenWrt 25.12.5 (apk-tools, version de paquet `0.1.29-r1`, contenant l'agent 0.1.29) :

| target | Architecture du paquet | Fichier |
|---|---|---|
| ipq40xx/generic | arm_cortex-a7_neon-vfpv4 | `remote-agent-openwrt-ipq40xx-official.apk` |
| x86/64 | x86_64 | `remote-agent-openwrt-x86_64-official.apk` |
| armsr/armv8 | aarch64_generic | `remote-agent-openwrt-armsr-armv8-official.apk` |
| mediatek/filogic | aarch64_cortex-a53 | `remote-agent-openwrt-mediatek-filogic-official.apk` |
| bcm27xx/bcm2711 | aarch64_cortex-a72 | `remote-agent-openwrt-bcm27xx-bcm2711-official.apk` |
| ramips/mt7621 | mipsel_24kc | `remote-agent-openwrt-ramips-mt7621-official.apk` |
| ath79/generic | mips_24kc | `remote-agent-openwrt-ath79-official.apk` |

L'interface LuCI est commune : `luci-app-remote-openwrt-official.apk`. Vérifiez l'architecture avec `apk --print-arch`, puis utilisez `apk add --allow-untrusted ./文件.apk`. Les paquets MIPS dépendent aussi de `libgcc1` (inclus par défaut dans les images officielles).

## Linux musl (Alpine) et arm64

- `remote-linux-arm64.tar.gz` : aarch64, glibc 2.28+ (Debian 10+/Ubuntu 18.04+/RHEL 8+, Raspberry Pi 64 bits, etc.), avec enregistrement systemd via `remote-agent install` et mise à jour automatique.
- `remote-linux-musl-x64.tar.gz` / `remote-linux-musl-arm64.tar.gz` : musl lié statiquement pour les distributions sans glibc comme Alpine. Alpine n'a pas systemd par défaut ; utilisez `remote-agent install --no-start` pour écrire la configuration, puis ajoutez votre propre service OpenRC (`/etc/init.d/remote-agent`) ou lancez-le au premier plan. Les versions musl prennent en charge `update self` et la mise à jour automatique de bureau, et n'installent que l'archive musl ; après une mise à jour ponctuelle, elles essaient systemd, puis OpenRC (`rc-service remote-agent restart`), puis un redémarrage du processus. Une archive musl sur un hôte OpenWrt se met toujours à jour via opkg/apk.

## Linux

Utilisez de préférence la sous-commande intégrée : elle copie l'exécutable actuel, écrit une configuration privée et enregistre systemd sans dépendre des fichiers `install/` du dépôt :

```sh
cargo build --locked -p agent --no-default-features --profile release-small
# 用户级（默认，非 root）
./target/release-small/remote-agent install \
  --config /path/to/config.json
# 或直接传凭证（无需先有文件；安装过程不访问网络）
./target/release-small/remote-agent install \
  --device-id NAME --token TOKEN

# 系统级（root 或 --system）：/usr/local/bin + /etc/remote-agent + multi-user.target
sudo ./target/release-small/remote-agent install --system \
  --device-id NAME --token TOKEN

remote-agent status                    # 安装布局、服务、进程、连接（不打印令牌）
systemctl --user status remote-agent   # 用户级
# systemctl status remote-agent        # 系统级
remote-agent uninstall                 # 卸服务；加 --force 同时删二进制与配置
```

Étapes manuelles équivalentes (toujours utilisables) :

```sh
install -Dm755 target/release-small/remote-agent ~/.local/bin/remote-agent
install -Dm600 /path/to/config.json ~/.config/remote-agent/config.json
install -Dm644 install/linux/remote-agent.service ~/.config/systemd/user/remote-agent.service
systemctl --user daemon-reload
systemctl --user enable --now remote-agent
```

Par défaut, le service utilisateur suit la session de connexion. Pour fonctionner dès le démarrage, un administrateur doit lancer `loginctl enable-linger <user>`, ou utiliser l'unité système `--system` ci-dessus (`RuntimeDirectory=remote-agent`, `WantedBy=multi-user.target`). Les commandes distantes utilisent les permissions du compte du service. Arrêter le service récupère tous ses processus. `install/linux/install.sh` est un simple wrapper de `remote-agent install --force`.

## Windows

Après installation de la chaîne Rust MSVC et de Visual Studio C++ Build Tools :

```powershell
cargo build --locked -p agent --release
.\target\release\remote-agent.exe --config C:\path\config.json

# 推荐：为当前登录用户安装常驻计划任务
.\target\release\remote-agent.exe install --config C:\path\config.json
# 已存在任务/文件时加 --force；仅注册不启动加 --no-start
Get-ScheduledTaskInfo -TaskName RemoteAgent
remote-agent status
remote-agent uninstall
remote-agent uninstall --force   # 同时删除 %LOCALAPPDATA%\remote-agent 下的文件
```

`install` copie les binaires et la configuration dans `%LOCALAPPDATA%\remote-agent`, limite l'ACL du fichier et enregistre la tâche `RemoteAgent` à la connexion. Par défaut, une tâche ou un fichier cible existant provoque une erreur ; `--force` arrête et supprime d'abord l'ancienne tâche avant remplacement. Ce mode nécessite une session utilisateur ; ce n'est pas un service système Windows SCM. `install/windows/install.ps1` est un simple wrapper de `remote-agent install` (avec `-Force` si nécessaire).

Si la configuration a d'abord été créée par l'interface, `--config` peut désigner un chemin distinct (par exemple le fichier créé avec `--ui --config .\device-config.json`).

Les commandes Windows utilisent PowerShell, par exemple `Get-Process`, et leur sortie est envoyée en UTF-8. Chaque exécution et terminal possède un Job Object propre ; l'arbre de processus enfants est nettoyé à l'expiration, l'annulation, la déconnexion ou la sortie de l'agent.

## macOS

La chaîne Rust est nécessaire (`rustup`, avec la cible par défaut `aarch64-apple-darwin` ou `x86_64-apple-darwin`). Les métriques sont collectées via sysctl, `getloadavg(3)` et mach `host_statistics` ; les informations statiques (modèle, noyau, version système) via `sysctl` et `sw_vers`. La plateforme signalée est `macos`.

Les capteurs à la demande (hors battements) sont collectés au mieux sans sudo ni dépendance SMC supplémentaire : batterie des portables via `pmset -g batt`, CPU_Speed_Limit / Scheduler / Available CPUs et texte de pression thermique via `pmset -g therm`, AppleSmartBattery `Temperature` et régime des ventilateurs via `ioreg` si le firmware les expose. Les ordinateurs fixes tels que Mac mini n'ont souvent pas de batterie ; Apple Silicon peut ne pas fournir de CPU_Speed_Limit numérique. Les températures brutes du die CPU / de proximité nécessitent IOKit/SMC privés et ne sont pas collectées. Une mesure de pression thermique, de limitation de fréquence ou de ventilateur suffit pour ne pas marquer `thermal` indisponible. Avec `desktop-ui`, `remote-agent --ui` permet de configurer et d'activer/désactiver le démarrage à la connexion (LaunchAgent).

```sh
cargo build --locked -p agent --release

# 推荐：安装为随登录会话运行的 LaunchAgent（无需 sudo）
./target/release/remote-agent install --config ~/.config/remote-agent/config.json
# 或：install/macos/install.sh ~/.config/remote-agent/config.json
remote-agent status
launchctl print gui/$(id -u)/us.ivjn.remote-agent | grep state
```

`remote-agent install` copie l'exécutable actuel dans `~/.local/bin/remote-agent`, écrit `~/.config/remote-agent/config.json` (permissions 600), crée `~/Library/LaunchAgents/us.ivjn.remote-agent.plist`, puis exécute `codesign -s - --force` sur le binaire (macOS tue les remplacements sur place non signés), suivi de `launchctl bootstrap` / `kickstart`. `RunAtLoad` + `KeepAlive` lancent l'agent à la connexion et permettent à launchd de le relancer après plantage ou déconnexion. Les journaux vont dans `~/Library/Logs/remote-agent.log`. `install/macos/install.sh` est un simple wrapper. Sans fichier d'état explicite, `$HOME/.local/state/remote-agent/agent.state` est utilisé. Les commandes distantes ont les permissions de cet utilisateur connecté.

Arrêt / désinstallation :

```sh
remote-agent uninstall
# 或手动：
# launchctl bootout gui/$(id -u)/us.ivjn.remote-agent
# rm ~/Library/LaunchAgents/us.ivjn.remote-agent.plist
```

Pour fonctionner dès le démarrage sans utilisateur connecté, utilisez un LaunchDaemon dans `/Library/LaunchDaemons`, chargé en root ; les commandes distantes s'exécutent alors en root. Évaluez le risque en conséquence.

## APK Android autonome

Le projet de l'appareil est dans `android-agent/`. `mobile/` reste le client d'administration ; les deux s'installent séparément, sans Expo Go, Termux ni root.

L'adresse par défaut de l'Agent sur toutes les plateformes est `wss://exchange.ivjn.us`. Sous Android, **Connect with a six-digit code** permet d'obtenir automatiquement l'identité après autorisation par un administrateur saisissant les six chiffres dans le CLI. Les Agents natifs (page locale Windows/macOS, « Ajouter un appareil » dans LuCI OpenWrt, commandes `remote-agent pair` / `remote-agent join <邀请>`) peuvent afficher un code à six chiffres et un QR code à approuver depuis Console, ou accepter une invitation à usage unique. Voir [Initialisation et appairage](/docs/fr/pairing-and-enrollment.md).

### Première connexion et accueil

1. Ouvrez **Remote Agent** sur l’appareil Android à gérer. Pour la première configuration, choisissez **Se connecter avec un code à six chiffres** ou **Scanner le code QR de connexion** et obtenez l’approbation d’un administrateur. Voir [Initialisation et appairage](/docs/fr/pairing-and-enrollment.md) pour les comptes, QR codes et codes à six chiffres.
2. Si vous disposez déjà d’un ID et d’un jeton d’appareil, ouvrez **Paramètres → Paramètres de connexion**, renseignez le serveur et les identifiants, puis choisissez **Enregistrer et se connecter**. Les champs sont masqués lorsque la connexion est activée. Déconnectez l’appareil depuis l’accueil avant de modifier son identité.
3. L’accueil affiche l’état de connexion, le partage d’écran et la liste des gestionnaires. Si la configuration est enregistrée mais la connexion désactivée, choisissez **Connecter l\'appareil**. Rouvrir l’application rétablit une connexion activée.

Android 12 et versions ultérieures utilisent l’écran de démarrage système ; Android 8–11 affichent un fond de lancement aux mêmes couleurs. L’accueil s’ouvre dès que la première image est prête, sans attendre la connexion réseau ni imposer une durée minimale. Les transitions sont ignorées si les animations système sont désactivées. Les réglages sont chargés à la demande ; ouvrir l’accueil n’accorde aucune autorisation d’accessibilité ou de partage d’écran. Cette description concerne le code actuel ; vérifiez la version installée pour les téléchargements publics.

### Arrière-plan, notifications et arrêt

Passer en arrière-plan ou fermer l’interface ne termine pas le service de premier plan. L’Agent se reconnecte après une coupure réseau. Lorsque la connexion est activée, le redémarrage et le déverrouillage de l’appareil, ou une mise à jour de l’application, déclenchent une tentative de reprise du service. Après un arrêt forcé, rouvrez l’application manuellement. Le fabricant peut aussi exiger une autorisation d’exécution en arrière-plan.

Si les notifications sont autorisées, **Gérer la connexion** ramène à l’application. Pendant un partage, **Arrêter le partage** dans la notification termine uniquement le partage d’écran. Pour déconnecter l’accès distant et désactiver la reprise automatique, choisissez **Déconnecter** sur l’accueil et confirmez. La configuration est conservée pour une reconnexion ultérieure via **Connecter l\'appareil**. **Arrêter le partage d\'écran** sur l’accueil termine également le partage sans déconnecter l’accès distant.

Le jeton d’appareil est chiffré avec Android Keystore AES-GCM. La configuration est exclue des sauvegardes et les Activities ou Services exportés ne transmettent pas de jetons. Les commandes distantes s’exécutent sous l’UID de l’APK, avec les restrictions du bac à sable Android et de SELinux. Elles ne donnent accès ni aux données des autres applications ni aux privilèges root ; les réglages système, l’accessibilité et le partage nécessitent leurs propres autorisations.

Pendant l’accès distant, le service garde les verrous de veille du processeur et de l’écran. L’écran reste allumé en arrière-plan et durant les reconnexions, ce qui augmente la consommation. La déconnexion ou la destruction du service libère les verrous et rétablit l’extinction normale de l’écran. Le bouton d’alimentation verrouille toujours l’appareil sans contourner l’authentification. Les notifications indiquent la connexion ou le partage ; la déconnexion se fait depuis l’accueil.

### Compiler depuis les sources

Démarrez chacun des exemples Debug ou Release suivants à la racine du dépôt. Vérifiez la liste des appareils avant installation et remplacez DEVICE_SERIAL par le numéro de série ciblé. La création de l’APK, les tests unitaires et le Lint complet sont des contrôles distincts. En cas d’échec de Lint, consultez le rapport et corrigez ou documentez la cause ; ignorer la tâche ne vaut pas réussite.

La compilation nécessite JDK 17+, Android SDK Platform 36, NDK `28.2.13676358`, Python 3 et Rust. Définissez `ANDROID_HOME` (ou `ANDROID_SDK_ROOT`) ; utilisez `ANDROID_NDK_HOME` pour un chemin NDK personnalisé. L'affichage plein écran `.Aran` dépend du SDK Android Metafile : placez le dépôt `Metafile` au même niveau que `remote-control` (Gradle référence `:aran` via `../../Metafile/android-sdk/aran`).

```sh
rustup target add aarch64-linux-android x86_64-linux-android
cd android-agent
./gradlew :app:assembleDebug :app:testDebugUnitTest
# Windows: .\gradlew.bat :app:assembleDebug :app:testDebugUnitTest
./gradlew :app:lintDebug
# Windows: .\gradlew.bat :app:lintDebug
adb devices -l
adb -s DEVICE_SERIAL install -r app/build/outputs/apk/debug/remote-agent-debug.apk
```

Gradle appelle automatiquement `scripts/build-android-agent.py` pour placer les exécutables Rust des deux ABI dans le répertoire des bibliothèques natives de l'APK. Après installation, ils sont lancés directement depuis ce répertoire en lecture seule, conformément à l'interdiction Android d'exécution dans les données d'application. L'APK debug sert à la validation ; pour distribuer, compilez une release avec la signature de votre organisation et conservez la clé pour les mises à niveau.

La compilation Release lit `android-agent/keystore.properties` (ignoré par Git) ou une configuration externe désignée par `REMOTE_AGENT_SIGNING_PROPERTIES`. Les champs sont dans `android-agent/keystore.properties.example` ; conservez les clés hors du dépôt. Sans configuration de signature, Release échoue sans revenir à la signature Debug.

```powershell
cd android-agent
$env:REMOTE_AGENT_SIGNING_PROPERTIES = "$env:USERPROFILE/.remote-control/signing/remote-agent.properties"
.\gradlew.bat :app:assembleRelease :app:lintRelease :app:testReleaseUnitTest --no-daemon
# Output: app/build/outputs/apk/release/remote-agent-release.apk
```

Sauvegardez la clé de publication et son mot de passe de manière sûre : les versions suivantes doivent conserver cette clé pour une mise à niveau sur place. Debug et Release ont des signatures différentes ; une installation Debug ne peut pas être directement remplacée. La désinstallation efface la configuration et les identifiants locaux : prévoyez un nouvel appairage. `scripts/build-site-downloads.py` utilise l'APK Release signé comme téléchargement Agent. La signature Release ne garantit pas la levée d'un blocage Play Protect.

### Connexion Android par QR code

**Scan connection QR code** sur l'accueil de Remote Agent reconnaît les QR codes hors ligne avec la caméra, sans services Google Play. Autorisez la caméra au premier usage ; annuler ne modifie pas la configuration. Après lecture, serveur et ID sont affichés, mais le jeton reste masqué. La configuration n'est chiffrée, enregistrée et connectée qu'après **Connect** ; arrêtez toute connexion existante avant d'en importer une nouvelle. Le scan n'active ni contrôle d'écran ni accessibilité.

Enregistrez d'abord un appareil distinct dans Exchange et obtenez son **jeton d'appareil**, puis générez le QR code localement sur l'ordinateur (sans jeton de compte opérateur ni site tiers de génération en ligne) :

```powershell
python -m pip install qrcode==8.2
python scripts/connection-qr.py --server wss://exchange.example.com --device-id android-phone --output "$env:TEMP\android-phone-connection.svg"
# 按提示输入设备令牌，输入不会显示。也可用 --token-stdin 从标准输入读取。
```

Ouvrez le SVG généré sur l'ordinateur et scannez-le avec Remote Agent. Le fichier contient le jeton : gardez-le confidentiel, supprimez-le après usage et ne le commitez pas dans Git. L'outil refuse d'écraser un fichier existant.

Le QR code contient un JSON UTF-8 limité à 2048 octets :

```json
{"type":"remote-agent","version":1,"server":"wss://exchange.example.com","device_id":"android-phone","token":"DEVICE_TOKEN"}
```

Le scan accepte uniquement `wss://`, éventuellement avec `/ws/device`, sans informations utilisateur, paramètres de requête ni fragment d'URL. `server`, `device_id`, `token` doivent être des chaînes non vides de 512, 128 et 512 caractères maximum, sans caractères de contrôle ni espaces initiaux/finals. La configuration manuelle reste utilisable pour les tests locaux en `ws://`.

### Accessibilité Android, captures d'écran et saisie de texte

Utilisez `remote-cli` pour les commandes suivantes. L’appareil annonce `android_control` via le pont interne de l’APK ; consultez `status` pour les autorisations.

1. Connectez l’appareil, ouvrez **Paramètres → Écran et interaction** et activez **Autoriser les commandes à distance de l\'écran** (désactivé par défaut).
2. Choisissez **Paramètres d\'accessibilité** et activez Remote Agent dans Android. Cela autorise la lecture des arbres d’interface accessibles, les appuis, balayages, la navigation système et le remplacement du texte du champ actif.
3. Android 11+ peut capturer via l’accessibilité activée. Sous Android 8–10, ou pour la capture seule, choisissez **Partager l\'écran** sur l’accueil ou **Partage d\'écran standard** dans les réglages, puis confirmez le dialogue système. L’autorisation doit être renouvelée après la fin du partage ou le redémarrage du processus.

```sh
remote-cli ui my-device status
remote-cli ui my-device tree
remote-cli ui my-device xml > screen.xml
remote-cli ui my-device click --view-id com.example:id/search
remote-cli ui my-device click --text "Search" --description "Open search"
remote-cli ui my-device screenshot --output screen.jpg
remote-cli ui my-device tap 300 500
remote-cli ui my-device swipe 500 1500 500 500 --duration-ms 400
# 多段滑动（折线，2-64 个点）
remote-cli ui my-device swipe-path 300,1500 300,800 900,800 --duration-ms 800
remote-cli ui my-device key back
remote-cli ui my-device text "Hello"
remote-cli ui my-device text --stdin < message.txt
remote-cli ui my-device type --stdin < message.txt
```

`text` utilise Android `ACTION_SET_TEXT` pour **remplacer tout le texte du champ actif**. Chinois, emoji et lignes multiples sont acceptés, jusqu'à 8192 octets UTF-8, sans NUL. Cliquez d'abord sur un champ modifiable ; un contrôle non actif, non modifiable ou incompatible renvoie une erreur. `--stdin` lit l'UTF-8 en conservant les retours à la ligne et évite le texte dans l'historique des commandes. `key` accepte `back`, `home`, `recents`, `notifications`, `quick-settings`.

`xml` renvoie un arbre imbriqué `<hierarchy><node ...>` avec `resource-id`, `text`, `content-desc`, `bounds` et état des contrôles : maximum 200 nœuds, 50 niveaux et 256 caractères par attribut ; réponse et élément racine signalent toute troncature. `click` effectue une **correspondance exacte de la valeur complète** (pas XPath) : `--view-id` correspond à `resource-id`, `--description` à `content-desc` ; les conditions multiples sont combinées par AND. Seuls les contrôles visibles et activés correspondent ; zéro ou plusieurs résultats provoquent une erreur, et aucun clic n'a lieu si le parcours dépasse sa limite. Une étiquette de texte recherche un parent cliquable ; sinon utilisez `tap X Y`. Rechargez le XML après un changement d'interface ; préférez l'ID de ressource pour les attributs longs.

Pour la méthode de saisie, ouvrez **Paramètres → Remote Input**. Choisissez **Activer Remote Input** pour activer Remote Input dans Android, puis **Sélectionner la méthode de saisie** pour le sélectionner. Après activation du champ cible, `type` insère le texte au curseur / dans la sélection avec `InputConnection.commitText`, avec chinois, emoji et lignes multiples, et la même limite de 8192 octets UTF-8 que `text`. Il n'envoie pas automatiquement de message, ne soumet pas de formulaire et ne modifie pas la configuration de l'Agent. **Switch keyboard** rétablit votre clavier habituel. `status` renvoie `ime_enabled`, `ime_selected` et `ime_connected`. Allumez et déverrouillez l'appareil avant de lire/cliquer l'interface ou saisir du texte.

La page **Écran de l'appareil** de Remote Console propose clics par coordonnées, XML et clics par attribut, ainsi que saisie Remote Input. Les boutons sont désactivés si l'état de l'écran ou les autorisations ne conviennent pas ; une saisie réussie efface le texte. Le passage en arrière-plan efface captures, XML et texte en attente.

### Capacités système

Les paramètres utilisent les fournisseurs annoncés par l'appareil ; les espaces de noms sont des chaînes, pas une énumération figée. Android conserve `android_settings` avec `system`, `secure`, `global`. Linux (hors OpenWrt) et macOS utilisent `system_settings` : gsettings de l'utilisateur actuel sous Linux, `/usr/bin/defaults` sous macOS. L'extension de navigateur utilise `browser_settings` pour lire/écrire `privacy.network`, `privacy.services`, `privacy.websites` via une session chiffrée. Windows et OpenWrt ne fournissent pas ces paramètres. Pour ajouter des paramètres clé-valeur, enregistrez un fournisseur dans l'Agent ou l'extension ; le client génère son interface à partir du manifeste.

Les trois espaces Android sont indépendants de l'interrupteur d'écran, de l'affichage et du verrouillage ; ils passent par ContentResolver plutôt que par concaténation shell. Dans un APK ordinaire, écrire `system` exige de cliquer sur **Autoriser la modification des paramètres système** dans l'Agent et d'accorder l'accès ; écrire `secure/global` exige aussi `WRITE_SECURE_SETTINGS`. L'état retourne permissions et `providers`, mais Android ou le fabricant peut encore refuser une opération individuelle.

`settings list --prefix` filtre les noms sous Android. Sous Linux / macOS, sans préfixe, il retourne uniquement les noms de schémas ou domaines ; avec préfixe, les chaînes, entiers et booléens du premier niveau du schéma ou domaine choisi. `settings clear` n'est accepté que pour la confidentialité du navigateur ; les autres fournisseurs renvoient une erreur explicite. L'audit enregistre espace de noms, nom et nombre de caractères, sans valeur.

```sh
remote-cli ui realmev5 settings
remote-cli ui realmev5 settings list --namespace system --limit 100
remote-cli ui realmev5 settings get screen_off_timeout
remote-cli ui realmev5 settings set screen_off_timeout 600000
remote-cli ui realmev5 screen-timeout 600
remote-cli ui realmev5 settings get device_name --namespace global
remote-cli ui realmev5 settings set vendor.custom_setting --stdin
```

La valeur de `settings set` est une chaîne, et `--stdin` conserve les retours à la ligne ; elle n'est pas écrite dans l'audit. Maximum 8192 octets UTF-8 sans NUL ; nom limité à 256 caractères ASCII (lettres, chiffres, souligné, point, deux-points, tiret). Une liste est limitée à 500 éléments et 512 KiB, avec `truncated` au-delà ; une valeur individuelle à 64 KiB. Un paramètre absent retourne `value: null`. L'interface ne force pas silencieusement les paramètres restreints : chaque écriture est relue pour confirmation et un échec renvoie une erreur explicite. Les changements persistent après déconnexion. `screen-timeout` utilise des secondes (15–86400), tandis que `screen_off_timeout` utilise des millisecondes ; extinction automatique et délai de verrouillage après extinction sont distincts.

### Lecture multimédia (media)

Voix, audio, images (dont le texte plein écran), vidéo et Web utilisent `media`, annoncé par les Agents Android et Windows/Linux/macOS, mais pas OpenWrt. Le protocole définit trois actions : `media_play` (avec un objet `media`), `media_stop`, `media_status`, qui renvoient toutes le même état `media`.

| `media.type` | Champs | Canal |
| --- | --- | --- |
| `speech` | `text`, `language` facultatif (BCP-47, par exemple `zh-CN`) | Son |
| `audio` | Un seul de `url` ou `path`, `loop` facultatif | Son |
| `image` | Un seul de `url`, `path`, `text`, `p2p` facultatif (interdit avec `text`) | Écran |
| `video` | Un seul de `url` ou `path`, `p2p` facultatif | Écran |
| `web` | `url` (https uniquement) | Écran |

L'appareil possède deux canaux indépendants : **son** (voix, audio) et **écran** (images, texte, vidéo, Web). Un nouveau contenu remplace uniquement celui du même canal : musique et diaporama peuvent donc coexister. Le `channel` de `media_stop` accepte `sound`, `screen` ou `all` (par défaut).

```sh
remote-cli ui realmev5 media speak "Remote 连接正常" --language zh-CN
remote-cli ui realmev5 media speak --stdin --language zh-CN
remote-cli ui realmev5 media audio --path /sdcard/Music/doorbell.mp3
remote-cli ui realmev5 media audio --url https://cdn.example.com/notice.mp3 --loop
remote-cli ui realmev5 media image --text "值班中"
remote-cli ui realmev5 media image --url https://example.com/poster.jpg
remote-cli ui realmev5 media video --path /sdcard/loop.mp4
remote-cli ui realmev5 media video --url https://files.example.org/library/clip.Aran --p2p-group library --p2p-discovery https://p2p.ivjn.us --p2p-token secret --p2p-peer 192.0.2.10:7300
remote-cli ui realmev5 media web https://status.example.com/board
remote-cli ui realmev5 media status
remote-cli ui realmev5 media stop sound      # 或 screen；不写则两个通道都停
remote-cli ui laptop media audio --path 'C:\Users\me\Music\alert.wav'
remote-cli ui laptop media image --text "Kiosk"
```

`media status` renvoie :

```json
{"kind":"media",
 "sound":{"type":"audio","id":"…","state":"playing","language":null,"loop":true},
 "screen":{"type":"web","keep_awake":true},
 "speech_available":true,"media_volume":7,"media_volume_max":15}
```

`sound` est le dernier élément sonore (conservé après sa fin pour examiner le résultat), avec l'état `queued` / `speaking` / `loading` / `playing` / `completed` / `stopped` / `error`, ou `null` si rien n'a été joué. `screen` est présent uniquement pendant l'affichage, sinon `null`. `speech_available=false` signifie qu'aucun moteur vocal n'est disponible. Le volume multimédia n'a une valeur que sous Android ; il vaut 0 sur ordinateur.

Validation et audit : le texte (voix ou image) est limité à 4000 octets UTF-8 sans NUL ; l'URL doit être `https`, limitée à 2048 octets sans espaces ni caractères de contrôle ; le chemin est limité à 4096 octets sans caractères de contrôle. L'audit conserve uniquement type, catégorie de source (`url` / `path` / `text`), nombre de caractères, boucle et utilisation de P2P ; jamais URL, chemin ou texte (une URL peut contenir un jeton signé). Un compte `readonly` ne peut appeler aucune opération multimédia.

#### Son : synthèse vocale et audio

- Voix : Android utilise son moteur TTS ; Windows `System.Speech` ; macOS `say` ; Linux `spd-say` / `espeak-ng` / `espeak` (sans ces outils, `speech_available=false` et une erreur explicite). Sur ordinateur, les étiquettes de langue sont associées au mieux aux voix locales. Android ne répond qu'après le début de la parole et renvoie une erreur si l'initialisation ou les données linguistiques sont indisponibles ; arrêter l'Agent libère le TTS.
- Audio : chemin absolu sur l'appareil ou URL https. Transférez les fichiers absents avec `remote-cli files upload`, puis lisez-les par chemin. `loop` répète jusqu'à l'arrêt.
  - Android utilise `MediaPlayer` et le volume multimédia, avec lecture directe des URL en streaming ; la réponse attend le début du son, avec erreur après 10 secondes sans démarrage.
  - Sur ordinateur, l'URL est téléchargée dans le répertoire d'état temporaire de l'utilisateur (maximum 50 MB, redirections https uniquement), puis supprimée en fin de lecture, à l'arrêt ou sur erreur. L'interface répond au lancement du lecteur ; les échecs de téléchargement ou décodage apparaissent dans `media status` avec `sound.state=error`. Windows utilise `MediaPlayer` (mp3, wav, m4a, aac, wma, flac), macOS `afplay`, et Linux cherche `mpv`, `ffplay`, `paplay`, `pw-play`, `aplay` dans cet ordre ; sans lecteur, une erreur est renvoyée (`paplay` / `aplay` ne lisent pas mp3).
  - Le chemin doit être un fichier ordinaire existant et lisible ; Windows refuse les chemins réseau tels que `\\server\share` pour éviter d'envoyer des identifiants à un serveur SMB distant.
- Un volume nul peut rendre le son inaudible.

#### Écran : images, texte, vidéos et pages Web


- **image** : `path` local, URL `https` ou `text` (grand texte UTF-8 sur fond sombre, traité comme une image fixe). Sous Android, les sources `.Aran` sont déchiffrées par le SDK Android Metafile avant affichage.
- **video** : `path` local ou URL `https`, en boucle. Sous Android, `.Aran` n'est pas un flux HTTPS ordinaire : sans cache, il est déchiffré par blocs pendant la lecture et le clair est mis en cache pour la prochaine fois. Les blocs HTTP sont à `/meta/<SHA1>` à la racine de l'origine ; pour un fichier local, ils sont dans `meta/` à côté du manifeste. La clé embarquée dans le manifeste ou la clé SDK par défaut est utilisée. P2P facultatif : ordre des sources, `meta/` local, UDP P2P (`LOOKUP_ONLY`, sans inscription dans metastore), puis HTTP. Découverte et pairs statiques sont fournis par `media.p2p`, par exemple `--p2p-group library --p2p-discovery https://p2p.ivjn.us` ; un échec de démarrage P2P conserve HTTP/local sans interrompre la lecture.
- **web** : ouvre une page `https` en plein écran. Android utilise une WebView dans une Activity plein écran (JavaScript et DOM storage activés, navigation limitée à https ; un plantage du rendu ferme l'affichage sans affecter l'Agent). Sur ordinateur, un navigateur indépendant démarre en mode kiosk avec un profil temporaire et est terminé à l'arrêt : Windows cherche Edge, Chrome, Firefox ; macOS Chrome, Edge, Chromium, Firefox (Safari n'a pas de mode kiosk) ; Linux Chromium, Chrome, Edge, Firefox. Sans navigateur, une erreur explicite est renvoyée.

Sous Android, les manifestes et blocs `.Aran` de `https://cdn.ivjn.cn` et `https://cdn.ivjn.us` utilisent automatiquement l'autre domaine en secours, en conservant le chemin du manifeste ; les blocs restent dans `/meta/<SHA1>` de chaque origine. Aucun autre domaine n'est associé automatiquement. Pour les erreurs temporaires (dont HTTP 554), délais expirés et échecs de vérification de blocs, l'autre source est essayée avant un recul de 250/500 ms, sur 3 cycles maximum ; une source qui réussit devient prioritaire. Chaque connexion est limitée à 5 secondes, la lecture à 30 secondes ; les erreurs permanentes comme 403/404 ne réessaient pas la même source dans une même lecture. Taille des blocs et SHA-1 restent vérifiés, sans redirections ni assouplissement HTTPS. Cela nécessite le SDK Android Metafile `ab2b92f` ou ultérieur (avec `MirroredHttpSource`).

Le canal écran n'affiche qu'un élément à la fois ; un nouveau remplace le précédent. L'écran reste allumé pendant l'affichage (Android : `FLAG_KEEP_SCREEN_ON` + `turnScreenOn` / `showWhenLocked` ; Windows : `SetThreadExecutionState` ; macOS : `caffeinate -d` ; Linux : `systemd-inhibit` ou `xdg-screensaver inhibit`). Images fixes et texte se déplacent lentement de quelques pixels pour prévenir le marquage ; les vidéos bouclent et les pages Web restent fixes. Aucun cadre lumineux fixe. Android utilise une Activity plein écran distincte sans accessibilité. Linux exige une session X11 déverrouillée, avec `mpv` ou `ffplay` pour la vidéo ; macOS préfère mpv, sinon `open`.

#### Contrôle : position de lecture et pages Web

`media_control` contrôle le contenu du canal choisi et renvoie le même état `media`. `channel` doit être `sound` ou `screen` ; `op` est l'une des valeurs suivantes :

| `op.type` | Paramètres | Application |
| --- | --- | --- |
| `pause` / `resume` | — | Audio (sound), vidéo (screen) |
| `seek` | `position_ms` (position absolue, maximum 7 jours) | Audio, vidéo |
| `navigate` | `url` (https) | Web |
| `reload` / `back` / `forward` | — | Web |
| `scroll` | `dx`, `dy` (pixels, non nuls, maximum 100000 chacun) | Web |
| `eval` | `script` (maximum 16 KiB) | Web ; renvoie `script_result` (résultat encodé en JSON, maximum 64 KiB) |

```sh
remote-cli ui realmev5 media pause sound
remote-cli ui realmev5 media seek 1:30 screen      # 也接受 90、1:02:03.5
remote-cli ui realmev5 media resume screen
remote-cli ui realmev5 media web go https://status.example.com/next
remote-cli ui realmev5 media web back
remote-cli ui realmev5 media web scroll 0 800
remote-cli ui realmev5 media web eval "document.title"
```

L'état ajoute `position_ms`, `duration_ms`, `paused` à `sound` / `screen` pour l'audio et la vidéo si le lecteur les fournit, et `url`, `title` à `screen` pour le Web. Voix, images et texte ne sont pas contrôlables ; une opération incompatible avec le canal renvoie une erreur explicite. L'audit conserve canal, type d'opération et nombre de caractères du script, sans URL ni contenu du script.

- Android : audio et vidéo contrôlent directement `MediaPlayer` / `VideoView`, et le Web la WebView de l'Activity plein écran.
- Windows : l'audio est exécuté par le worker `MediaPlayer` intégré ; la vidéo mpv est contrôlée par un tube IPC, le lecteur WPF de repli est aussi contrôlable, mais pas ffplay.
- Linux / macOS : le contrôle audio/vidéo nécessite mpv (JSON IPC), sinon « Install mpv to pause or seek desktop media » est renvoyé ; avec mpv installé, macOS l'utilise aussi pour l'audio.
- Web sur ordinateur : le kiosk Chrome / Edge / Chromium démarre avec `--remote-debugging-port=0` (127.0.0.1 uniquement, port aléatoire, profil temporaire) et utilise DevTools. Firefox accepte seulement `navigate` en rouvrant le kiosk à la nouvelle adresse.

Dans les détails d'appareil de Remote Console mobile, **Capacités système** propose les onglets « Paramètres système » et « Médias ». Le second affiche les deux états, arrête séparément son/écran, lit voix, URL audio, texte, images, vidéos et Web, et contrôle position audio/vidéo et Web (navigation, précédent/suivant, actualisation, défilement, JS). Le passage en arrière-plan efface résultats de paramètres, valeurs à envoyer, texte vocal et saisies d'affichage. La console Web propose le même formulaire ; les paramètres du navigateur réutilisent la session chiffrée existante. Aucune page Web distincte de lecture multimédia n'est prévue.

### Recherche de texte et XML Android

```sh
remote-cli ui realmev5 xml
remote-cli ui realmev5 find-text "设置"
remote-cli ui realmev5 find-text "Settings" --exact --ignore-case
```

La recherche compare texte et description des nœuds visibles, par inclusion par défaut, avec correspondance exacte et insensibilité à la casse facultatives. Chaque résultat contient `bounds` et `center` (pixels de l'écran original, cliquables avec `tap X Y`), ID de ressource, texte et état d'activation. Le centre est calculé sur l'intersection avec l'écran pour éviter les coordonnées hors écran ; le parcours est limité à 200 nœuds et signale la troncature. Les nœuds de mot de passe et le sous-arbre de configuration de l'Agent sont exclus. Le texte dessiné par une application peut être inaccessible à Android ; aucune OCR n'est réalisée. Relancez la recherche après un changement d'interface.

Les captures sont des JPEG dont le plus grand côté est limité à 1600 pixels ; le CLI refuse d'écraser un fichier existant. Il affiche les dimensions de l'image et de l'écran original ; clics, balayages et `bounds` utilisent les pixels originaux. Pour viser depuis la miniature, utilisez `屏幕坐标 = 图像坐标 × screen_width / width` (rapport des hauteurs verticalement). Relisez les dimensions après rotation. L'arbre est limité à 200 nœuds et 256 unités UTF-16 par chaîne ; `truncated` indique la limite de nœuds. Une interface dessinée par l'application peut n'exposer aucun nœud accessible.

Le texte et la description des mots de passe et le sous-arbre de configuration de Remote Agent ne sont pas retournés ; la saisie distante ne peut modifier sa configuration. Les fenêtres protégées par Android `FLAG_SECURE` sont noires ou produisent une erreur système ; Android 15 peut aussi masquer automatiquement les applications sensibles contenant des mots de passe pendant le partage. Le contrôle réutilise l'authentification opérateur ; les comptes `readonly` ne peuvent pas l'utiliser. L'audit conserve uniquement action, coordonnées et nombre de caractères saisis, sans texte, arbre ni capture.

**Stop screen sharing** termine MediaProjection ; si l'accessibilité Android 11+ reste active, ses captures restent disponibles. Désactiver **Allow remote screen controls** bloque toutes les opérations ultérieures et termine le partage ; **Stop remote access** déconnecte aussi l'agent. Les clics et saisies déjà exécutés ne peuvent être annulés. L'autorisation d'accessibilité se gère séparément dans Android ; elle reste accordée après désactivation ou arrêt du service, mais aucune opération distante n'est acceptée.

Le pont local utilise un socket Unix au nom aléatoire et vérifie que l'appelant partage l'UID de l'APK. Cela protège contre les autres applications ordinaires ; le shell distant autorisé partage l'UID de l'APK. L'interrupteur d'écran ne constitue donc pas une séparation de privilèges vis-à-vis de ce shell ou de l'administrateur de l'appareil.

## Informations système et métriques

`DeviceInfo.platform` vaut `linux`, `freebsd`, `netbsd`, `windows`, `android`, `macos` ou `openwrt` ; `os_version` est la version générique du système et `capabilities` décrit `exec`, `pty`, `ubus`, `android_control`. OpenWrt transmet `openwrt_version`.

Linux/OpenWrt collectent les métriques dans `/proc`. Android conserve les autres métriques lisibles malgré les restrictions SELinux ; la durée de fonctionnement peut venir de `CLOCK_BOOTTIME`. Windows utilise les API natives pour mémoire, CPU, durée et compteurs réseau. Les métriques impossibles à collecter figurent dans `unavailable` et apparaissent manquantes dans le CLI et le mobile. Windows n'a pas de charge moyenne Unix : `load` est toujours indisponible.

## Validation

```sh
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo test -p agent --features desktop-ui
cargo clippy -p agent --features desktop-ui --all-targets -- -D warnings
cd mobile && npm run typecheck && npm run lint && npm test
```

`scripts/agent-smoke.py` est un relais local de test Python `websockets` qui valide sur un véritable agent enregistrement, métriques, exécution, expiration, annulation, PTY, resize et ubus non pris en charge. Il ne remplace pas les tests d'authentification et de base Exchange. Suivez ses instructions avec des identifiants de test fixes, sans appareil ni service de production. Windows et Linux doivent chacun exécuter leurs tests natifs ; sous Android, installez aussi l'APK et vérifiez premier plan, arrière-plan, arrêt et reconnexion dans l'environnement réel.

`scripts/agent-ui-smoke.py --agent <binary> --platform linux|windows` vérifie séparément configuration initiale, authentification et restrictions d'origine, enregistrement privé, conservation du jeton, enregistrement et exécution réels, nettoyage à l'arrêt, reconnexion et réouverture. Python 3.11+ et `websockets` sont nécessaires ; compilez d'abord avec `desktop-ui`. La validation navigateur doit aussi couvrir sauvegarde, erreurs de saisie, visibilité du jeton, restauration après actualisation et disposition étroite.

`scripts/android-control-smoke.py` utilise un véritable Exchange, le CLI et une application Android de test autonome pour valider les commandes d'écran. Exécutez-le uniquement sur un émulateur dédié et jetable : il efface les données de Remote Agent et modifie l'accessibilité. Compilez et installez `:app:assembleDebug` et `:control-fixture:assembleDebug`, préparez un Exchange / une base distincts, enregistrez l'appareil et sauvegardez son jeton et la configuration CLI ; Python 3.11+, Pillow et `adb` sont requis.

```sh
adb -s emulator-5580 install -r android-agent/app/build/outputs/apk/debug/remote-agent-debug.apk
adb -s emulator-5580 install -r -t android-agent/control-fixture/build/outputs/apk/debug/control-fixture-debug.apk
python scripts/android-control-smoke.py --serial emulator-5580 \
  --cli target/debug/remote-cli --cli-config /private/qa-cli.toml \
  --device-token /private/qa-device-token.txt \
  --device-id android-control-qa --device-server ws://10.0.2.2:18820/ws/device
```

Sous Windows, utilisez `target/debug/remote-cli.exe` pour le CLI. Les résultats vont par défaut dans `target/android-control-qa/screenshots/`. La fixture sert uniquement aux tests et n'est pas intégrée à l'APK de l'appareil.

### Journaux Android, état de l'écran et réveil

Remote Console ajoute **Journaux de l'appareil** et **Écran de l'appareil** aux détails. La page écran affiche présence, alimentation, verrouillage, dimensions et permissions, avec un aperçu JPEG à la demande et **Réveiller l'écran**. Les actions sont désactivées sans écran ou interface Android de contrôle ; l'absence d'autorisation de capture est indiquée séparément. Un écran éteint peut être réveillé si les commandes locales sont activées. Le réveil ne déverrouille jamais l'écran.

L'Agent Android propose localement **État de l'écran et journaux de l'appareil**. Pendant l'accès distant, champs de connexion, inscription QR et boutons de code sont masqués. La déconnexion nécessite confirmation et conserve les identifiants. Autorisation d'accessibilité et service effectivement lié sont signalés séparément ; si autorisé mais en attente de liaison, désactivez/réactivez seulement Remote Agent dans l'accessibilité système.

```powershell
remote-cli ui realmev5 status
remote-cli ui realmev5 logs --limit 100
remote-cli ui realmev5 wake
remote-cli ui realmev5 screenshot --output screen.jpg
```

L'interface WebSocket `open_android` existante accepte `{action:"logs",limit:100}` (1–300 entrées) et `{action:"wake"}`. L'état ajoute les champs facultatifs `display_present`, `screen_on`, `locked`, `screen_width`, `screen_height`, `accessibility_authorized` ; `accessibility_enabled` signifie toujours que le service est connecté. Les API d'écran Android natives exigent toujours l'APK ; les agents non Android n'annoncent pas cette interface.

Les journaux viennent désormais de `logcat -b all`, sans filtre de PID, paquet ou étiquette : tampons système, application, événements et plantages accessibles à logd Android. Chaque instantané retourne jusqu'à 100 entrées dans l'interface ou 1–300 dans le CLI, avec horodatages, niveaux, noms de tampon, PID/TID, étiquettes et messages originaux. Il s'agit de l'historique logcat actuel, pas d'archives de fichiers arbitraires ni de journaux déjà supprimés par rotation.

Accordez à l'APK la permission de journaux système via une connexion ADB autorisée :

```sh
adb shell pm grant com.remote.agent android.permission.READ_LOGS
```

Sur les versions Android exigeant une autorisation à chaque utilisation, ouvrez le diagnostic de l'Agent, acceptez la confirmation système et réessayez. L'accès en arrière-plan peut être refusé ou l'autorisation expirer selon Android/le fabricant. Après refus, Android peut retourner silencieusement seulement l'UID de l'appelant ; le collecteur examine donc les UID des 301 dernières entrées logcat avant de sélectionner la fin demandée. Sans autre UID (y compris journaux vides), il indique que l'accès à l'appareil complet n'a pas pu être confirmé plutôt que de présenter un instantané limité à l'application. Permission absente ou échec logcat renvoient aussi une erreur explicite ; aucun repli vers le tampon d'événements Agent. La collecte s'exécute hors du fil d'interface, s'arrête sur annulation, avec une limite de six secondes et de volume ; les instantanés trop grands signalent troncature ou erreur. Les journaux utilisent les sessions opérateur authentifiées, le routage locataire et les rôles existants. Leur texte peut contenir du contenu d'autres applications. Les vues Web et mobiles s'effacent à la sortie ou en arrière-plan, sans sauvegarde disque. Remote Console efface aussi les captures à ces moments.

**Partage plein écran transparent :** dans l'Agent Android, choisissez **Partager tout l'écran avec une superposition transparente**. Accordez l'affichage au-dessus des autres applications et le partage d'écran Android. L'Agent passe en arrière-plan et installe une superposition plein écran `TYPE_APPLICATION_OVERLAY`, d'opacité nulle, avec `FLAG_NOT_TOUCHABLE | FLAG_NOT_FOCUSABLE` : tous les touchers atteignent l'application dessous. MediaProjection capture tout l'affichage (Android 14+ demande explicitement l'affichage par défaut). Arrêtez depuis la page Agent ou la notification ; l'arrêt du partage ne déconnecte pas l'Agent. La superposition disparaît à la fin du partage ou à l'arrêt du service.

Les pages d'aperçu et d'état connecté autorisent les captures. Configuration de connexion, tickets et QR de jeton restent protégés. Android peut encore renvoyer une image noire pour la fenêtre sécurisée d'une autre application ; la superposition transparente ne contourne pas cette protection.

Avant capture, l'Agent Android vérifie l'état d'affichage et l'état interactif PowerManager. Il réveille uniquement un écran éteint, confirme son passage à l'état interactif et refuse la capture tant que le verrouillage reste actif. Le lecteur MediaProjection consomme continuellement les images et conserve une image composée actuelle, au lieu de recréer sa surface à chaque capture. Face à des images entièrement noires, il attend une image exploitable puis renvoie une erreur avec indication d'action à l'expiration, plutôt qu'un JPEG noir réussi.

## Comparaison des modes Agent

Le mode de lancement et la plateforme sont deux dimensions distinctes. Les modes natifs headless, UI locale et service système partagent le cœur de connexion ; `desktop-ui` ajoute uniquement la configuration locale, sans accorder de droits distants.

| Mode/plateforme | Capacités | Conditions et limites |
|---|---|---|
| headless / UI locale | Même protocole natif, commandes, terminal, fichiers, métriques, notifications, LAN facultatif | UI sur Linux/Windows/macOS uniquement ; icône Windows ; les actions de bureau exigent une session graphique utilisateur disponible |
| Service système / lancement à la connexion | Même Agent natif | Hérite du compte et de l’environnement du service ; les paramètres de lancement ne sont pas copiés vers un autre service |
| OpenWrt | Cœur natif, ubus | Annonce ubus lorsqu’il est disponible ; supervision procd ; pas de contrôle du bureau annoncé |
| Linux / Windows / macOS | Cœur natif, contrôle du bureau, journaux système, médias | Dépend des autorisations/outils ; paramètres système annoncés uniquement sur Linux/macOS |
| FreeBSD / NetBSD | Cœur natif, terminal, fichiers, réseau, métriques, LAN facultatif | Batterie indisponible ; dépendances X11 ; sans médias/paramètres système |
| Android APK | Cœur Rust + passerelle Java, écran, paramètres, médias, OTA | Limites de l’UID applicatif et des permissions ; le binaire Rust seul n’a pas la passerelle de contrôle APK |
| Extension Chromium | URL, cookies, pages, paramètres du navigateur, CDP, métriques | Pas de commandes système/PTY/fichiers hôte arbitraires/écoute LAN ; CDP complet soumis à permission locale |

UI et headless partagent la résolution de configuration : CLI/environnement priment sur JSON pour LAN, watchdog, TLS et notifications ; la mise à jour automatique conserve CLI > JSON > `REMOTE_AUTO_UPDATE` > activée par défaut. `REMOTE_WATCHDOG=1`, 0, true/false, yes/no et on/off sont acceptés. L’UI refuse toujours les remplacements au lancement des identifiants de connexion et des chemins d’état. Les remplacements d’exécution ne sont pas enregistrés dans JSON ; le démarrage automatique nécessite une configuration séparée. Le statut LAN montre les valeurs effectives ; enregistrer la page ne supprime pas les paramètres de lancement.

UI et headless utilisent `state_file` du JSON, sinon le même chemin système persistant par défaut ; chaque instance d’appareil doit avoir son propre chemin. La connexion affiche connecting et l’arrêt stopped. L’arrêt annule le future de mise à jour de cette connexion et libère l’état watchdog ; les tâches de surveillance s’arrêtent au contrôle suivant. Les opérations déjà terminées ne peuvent pas être annulées. Chaque reconnexion recharge aussi l’identité et la politique de confiance ; un échec de chargement efface l’identité précédente au lieu de conserver l’ancienne autorisation de l’appareil.

Toutes les opérations exigent des sessions scellées et une autorisation de la racine de l’espace de travail. Le consentement par appel/l’autorisation sans surveillance du CDP complet n’est pas isolé par les portées de cookies typées et ne contourne pas les règles Chrome. Une fermeture ou déconnexion exige une nouvelle négociation. Les archives de mise à jour correspondent à Windows x64, Linux glibc x64/arm64 et macOS arm64 ; les autres plateformes natives utilisent les installateurs/gestionnaires de paquets, Android utilise APK OTA et les navigateurs les paquets d’extension. Une capacité annoncée ne garantit pas la disponibilité du matériel, des droits ou d’une archive publiée.

## Écran de veille automatique

Les réglages de l’Agent local et de l’extension proposent l’inactivité (300 secondes par défaut), le début/la fin du contrôle et le démarrage immédiat. Affichez du texte, des images/vidéos HTTPS ou du HTML statique isolé. Les vidéos bouclent sans son ; le texte et les images peuvent se déplacer largement. Desktop et Android proposent un mot de passe de sortie. Le plein écran du navigateur n’a pas de mot de passe : touche, mouvement de souris ou fermeture suffisent. Seul l’écran actif est couvert ; ce n’est pas un verrouillage système. Linux nécessite X11 et xprintidle. OpenWrt sans bureau, Wayland et BSD ne prennent pas en charge cet affichage.

Mode couleur unie sans texte ni média : fond #RRGGBB et luminosité 0–100% (noir à 0%). Android réduit aussi le rétroéclairage de la fenêtre et le rétablit à la première interaction. Ailleurs, seule l’image est assombrie ; l’économie LCD dépend du rétroéclairage. Le dialogue de sortie reste lisible.

Depuis 0.1.34, seuls le texte et les images parcourent une large zone du fond. Les grands contenus sont réduits si nécessaire à 44% au plus de la largeur et de la hauteur disponibles, afin que les positions aux coins opposés ne se chevauchent pas. HTML, vidéo et couleur unie restent fixes ; la préférence de réduction des mouvements suspend le déplacement. Tous les modes masquent le pointeur dès la première image, le rétablissent pour le mot de passe et le masquent après annulation. Windows actualise le pointeur dans la fenêtre toutes les 100 ms sans modifier le curseur système. Disponible en premier dans les paquets Windows et Browser Agent 0.1.34 ; consultez le manifeste pour les autres plateformes.
