# Linux, FreeBSD, NetBSD, Windows, macOS and Android devices

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

Linux (glibc/musl), FreeBSD, NetBSD, Windows, macOS and Android devices reuse `remote-agent`, Exchange, `remote-cli` and mobile operation and maintenance clients. The device side and CLI use common names; environment variables are uniformly `REMOTE_*`.

## One command installation (Windows/Linux/macOS/FreeBSD/NetBSD)

The official installation script downloads and verifies the package for your system, places the binaries in `~/.local/bin`, and then calls `remote-agent install` to register the Agent to start at login:

Choose the command for your role and copy each command separately.

**Install Agent on the managed device**

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

**Install CLI on the operator machine**

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

Windows runs in Git Bash. Pass Agent credentials after the pipe: `bash -s -- agent --device-id NAME --token TOKEN` (or already has `config.json` / `REMOTE_CONFIG`). The environment variables written in front of `curl` cannot reach bash. Linux system-level systemd can add `--system`. The script will automatically distinguish between glibc and musl (Alpine, etc.), and Linux arm64 (aarch64) is also supported; `REMOTE_LIBC=musl` can be used to force the selection of the musl package. FreeBSD / NetBSD: unprivileged installs remain in `~/.local/bin`; run `sudo remote-agent install --system --config CONFIG` to register rc.d. Desktop control requires an X11 user session with `python3` and `xdotool`. Use dedicated packages for Android and OpenWrt.


## Commands and services

The device and CLI commands are `remote-agent` and `remote-cli`. `remote-cli` writes to `remote-cli/config.toml` by default; an explicit `REMOTE_CLI_CONFIG` takes precedence.

The OpenWrt package, UCI file and init service are named `remote-agent`. The state file is `/var/run/remote-agent.state` and the configuration file is `/etc/config/remote-agent`. The LuCI plugin `luci-app-remote` depends on this package. `REMOTE_AGENT_SRC` sets the build source path.

| Equipment | Command execution | Interactive terminal | Resident mode | ubus |
|---|---|---|---|---|
| Linux | `/bin/sh -c` | Unix PTY | systemd or foreground process | OpenWrt only |
| Linux musl (Alpine 3.x, etc.), x64/arm64 | `/bin/sh -c` | Unix PTY | Foreground process or self-written OpenRC service (no systemd) | Not supported |
| FreeBSD 14+, amd64/arm64 | `/bin/sh -c` | Unix PTY | rc.d (root/system); foreground (user) | Not supported |
| NetBSD 10+, amd64 | `/bin/sh -c` | Unix PTY | rc.d (root/system); foreground (user) | Not supported |
| Windows 10 1809+/11, Server 2019+ | Windows PowerShell 5.1 | ConPTY + PowerShell | Scheduled task at login or foreground process | Not supported |
| macOS 11+, Apple Silicon/Intel | `/bin/sh -c` | Unix PTY | launchd LaunchAgent or foreground process | Not supported |
| Android 8.0+, arm64/x86_64 | `/system/bin/sh -c` | Unix PTY | Standalone APK foreground service | Not supported |

## General configuration

First register the device through `remote-cli device add <id>` and obtain the device token. Linux/Windows creates `config.json` that is only readable by the current running account:

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

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

Priority is command line/`REMOTE_SERVER`, `REMOTE_DEVICE_ID`, `REMOTE_TOKEN` environment variables, then JSON, and finally UCI fallback at OpenWrt only. JSON can also set `state_file`; command-line `--state-file` / `REMOTE_STATE_FILE` overrides it. When no status file is specified, OpenWrt still uses `/var/run/remote-agent.state`; other platforms use `remote-agent/agent.state` under `LOCALAPPDATA`, `XDG_STATE_HOME`, and `$HOME/.local/state` in sequence, and fall back to the temporary directory when there is no user directory. Multiple instances should each use independent state paths.

Production connections use `wss://`. `ws://127.0.0.1` in the example is for local verification only.

### Agent link notification

JSON can be set to `"notify_urls": ["https://example.invalid/notify"]`, or use `remote-agent --notify-url https://example.invalid/notify` (repeatable); the environment variable `REMOTE_NOTIFY_URL` accepts comma-separated addresses. Non-empty command line/environment variable list replaces JSON configuration; otherwise uses JSON and finally falls back to comma separated UCI `remote-agent.main.notify_url` only in OpenWrt. Disable POST when all sources are omitted or empty.

`agent.connected` is sent after successful registration, `agent.disconnected` is sent once when the established session ends, and only `agent.rejected` is sent when registration is explicitly refused; network failure before registration will not be notified. POST uses the same JSON objects as Exchange Web Push (`title`, `body`, `url`, `event`, `device_id`, `occurred_at`), but describes the agent Link instead of device online status: `event` is prefixed with `agent.` instead of `device.`, `url` is `""` (no Web Push console path), `tag` is not sent. The body of the disconnect and reject contains a brief summary of the security reason and does not include the device token; the time is RFC3339 UTC.

The notification is sent in the background. If it fails, only the event name is recorded, the target address is not recorded, and the agent is not prevented from reconnecting. The retry policy is similar to Exchange Web Push: up to 5 attempts, network errors and HTTP 408/429/5xx can be retried, 404/410 and other non-retryable 4xx are discarded; integer seconds `Retry-After` are preferred, otherwise exponential backoff starts at 5 seconds, and the delay is limited to 1–900 seconds. Only accept HTTP/HTTPS, do not follow redirects; do not put username, password or other credentials in the URL, addresses containing username/password will be rejected.


### Outbound proxy and connection keepalive

Both headless and `--ui` connect to Exchange through the same `client::run`. WebSocket reads the agent environment variables inherited by the agent process: take the first non-empty `HTTPS_PROXY` / `https_proxy`, `ALL_PROXY` / `all_proxy`, `HTTP_PROXY` / `http_proxy`; `NO_PROXY` / `no_proxy` matching hosts bypass the proxy. `remote-cli`'s WebSocket uses the same rules (#36/#37, #39). Logging in to self-starting or background services requires setting these variables in their running environment. Setting them in another terminal will not be passed to the started process.

When the device WSS passes through the proxy, it first establishes the **HTTP CONNECT** tunnel, and then uses rustls to connect to the target; the ALPN of the TLS ClientHello **only declares `http/1.1`**, which is consistent with the CLI. Negotiating `h2` will break the current HTTP/1.1 WebSocket Upgrade path; when stuck on a connection under a Clash class proxy, you should check if the proxy supports CONNECT and use a build that includes the #39/#42 fix. Neither the device nor the CLI's WebSocket path supports SOCKS, please use an HTTP CONNECT proxy (e.g. `HTTPS_PROXY=http://127.0.0.1:7890`).

After the direct connection or CONNECT is completed, the agent enables `TCP_NODELAY` and TCP keepalive after being idle for 30 seconds; it also sends WebSocket Ping for each JSON heartbeat, and explicitly flushes the automatically queued Pong after receiving the Ping. Connection establishment and waiting for the registration response each have a 45-second timeout; sends and post-registration Pong flushes have a 30-second timeout. For details, see [Keep-alive and timeouts](/docs/en/websocket-protocol.md#keepalive-and-timeouts) (#42).

Windows builds with `desktop-ui` use the multithreaded Tokio runtime to schedule axum, the local tray and the device WebSocket concurrently, including in headless mode. On other platforms with this feature, the local UI (explicit `--ui` or a bare launch) also uses multiple threads. Builds without the feature and headless runs with explicit configuration on other platforms use `current_thread`, suitable for small devices.

## Linux / Windows / macOS configuration interface

Builds enable `desktop-ui` by default, and desktop releases must retain the local configuration UI. Use `--no-default-features` only for headless servers or embedded builds. While connected, the management server, device ID and device token inputs are hidden; stopping remote access shows them again. Connection status, LAN settings and start-at-login remain visible.

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

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

On Linux / Windows / macOS, if you start without connection parameters (for example, double-click `remote-agent.exe`), and `desktop-ui` is enabled: if there is no available configuration, the default `config.json` will be written (only the default Exchange is prefilled, device_id/token is left blank) and the browser configuration page will be opened; when configured, Windows uses the tray without forcing the browser to open, while Linux/macOS print the local URL (macOS also has a menu bar icon). An interactive launch without arguments without `desktop-ui` enabled will create a default configuration file and print the path before exiting to avoid displaying only `Missing device_id`. OpenWrt / systemd / scheduled tasks etc should still use `--config`, UCI or `REMOTE_*`.

Bare launches and explicit `--ui` use the same rule: open the default browser only when the saved configuration lacks a nonempty `device_id`/`token`. Configured agents connect in the background without opening another browser at login. Windows retains the tray entry for opening the page manually; Linux/macOS use the local URL printed in the terminal. `--ui --no-open` always suppresses automatic opening, including first setup. `--ui-port 8765` sets a fixed port; otherwise an available port is chosen randomly. The UI only listens on `127.0.0.1`. All assets are embedded, with no external fonts or CDNs.

The Windows tray and macOS menu bar provide entries for configuration, screensaver settings, task management, and clipboard actions. Task management opens the local task panel directly. Linux uses the browser configuration page.

First use `remote-cli device add <id>` to obtain the device ID and token, then fill in the server, device ID and device token in **Connection settings**:

- **Save only**: Save the configuration without connecting to the server.
- **Save and connect**: Save and connect; when the configuration is modified during operation, the old connection will be disconnected and the new configuration will be used to connect.
- **Stop remote access**: Disconnect, cancel current remote execution and terminal session; configuration retained.
- The status bar displays **Connected**, Connecting, Reconnecting or Stopped after successful registration, and displays the latest connection error.

When starting a connection, the UI will first write **Connecting** into its own state file, and then start the `client::run` task to avoid the page staying at the default value when the state file does not yet exist (#42).

The saved token will not be returned to the web page. Leave it blank to retain the original token. The configuration uses temporary file atomic replacement, Linux permissions are `0600`, and Windows ACL authorizes only the current user. When the configuration file JSON is damaged or unreadable, the interface prompts for repair and the original file will not be overwritten. The same configuration file can only be opened by one UI process.

The default configuration path is `$XDG_CONFIG_HOME/remote-agent/config.json` for Linux/macOS (`~/.config/remote-agent/config.json` when not set), and `%LOCALAPPDATA%\remote-agent\config.json` for Windows. `--ui --config <path>` or `REMOTE_CONFIG` specifies an existing JSON file. The UI does not accept `--server`, `--device-id`, `--token`, `--state-file` and corresponding environment variable overrides to avoid inconsistency between the saved value and the actual connection. The JSON `state_file` also applies to the UI; when absent, it uses the same persistent default as headless to preserve sealed identity and trust.

The configuration page provides the **Start at login/Login auto-start** switch (`autostart: { supported, enabled, detail }` from `GET /api/status`, `POST /api/autostart` from `{ "enabled": true|false }`). No elevated privileges are required:

- **macOS**: Install/remove `~/Library/LaunchAgents/us.ivjn.remote-agent.plist` (the template is the same as `install/macos/install.sh`), `launchctl bootstrap` / `bootout`; binary is copied to `~/.local/bin/remote-agent` first, and the configuration defaults to `~/.config/remote-agent/config.json` (retain if it exists).
- **Windows**: Register/unregister the current user scheduled task `RemoteAgent` (AtLogOn), logical alignment `install/windows/install.ps1`, installation directory `%LOCALAPPDATA%\remote-agent`.
- **Linux**: Install/remove user systemd units (`install/linux/remote-agent.service`), `systemctl --user enable --now`/`disable`.

When enabled, the current executable file will be copied to the agreed installation path when needed, and the existing `config.json` will be retained; the response and log will not print the token. The UI still manages the current process; after enabling self-start, LaunchAgent/scheduled tasks/systemd will pull up the background agent at the next login when exiting the UI.

**Closing the browser tab does not stop the Agent; exiting the Agent process stops its connection. UI startup automatically connects a configured device; first setup requires Save and connect.** The UI manages the current process and can enable or disable login autostart. Stop an existing background service before starting another UI process for the same device. After configuration and enabling autostart, the browser tab can be closed. Remote operations use the permissions of the account running the Agent.

The native URL contains the random access key for this UI, do not share it. The API also verifies Host and Origin and rejects calls from external web pages. The device token is stored in the configuration file and is not stored encrypted; it can still be read by the current account and administrator of the machine. The stripped-down build without the `desktop-ui` feature maintains the original command line behavior, and OpenWrt and Android use their respective existing interfaces.

## FreeBSD

The official website provides `remote-freebsd-x64.tar.gz` and `remote-freebsd-arm64.tar.gz` (cross-compile with `cargo zigbuild` on Linux and link to FreeBSD 14 libc). After decompression:

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

Current source supports rc.d system installation, sysctl metrics, `netstat -ibn` interface totals, FreeBSD temperature/fan readings and NetBSD `envstat`. X11 desktop control is advertised when `DISPLAY`, `python3` and `xdotool` are available; screenshots additionally need Pillow. Media and system settings remain unavailable. Self-update recognizes BSD x64/arm64 archives when a matching verified package exists in the release manifest. These changes require newly built packages; native BSD runtime verification remains necessary.

## NetBSD

The official website provides `remote-netbsd-x64.tar.gz` (amd64, using zig link NetBSD 10 libc cross-compilation on Linux, dynamic dependencies are only libc / libpthread / libutil). Usage is the same as FreeBSD:

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

Capabilities: connect to Exchange, `exec`/PTY/file transfer; indicators go through sysctl (`kern.cp_time`, `hw.physmem64`, `vm.uvmexp2`); desktop GUI control is implemented as stub, not announced. The device platform reports it as `netbsd`. Self-update and rc.d restart work as on FreeBSD (`remote-netbsd-x64.tar.gz` / `remote-netbsd-arm64.tar.gz`), provided the manifest lists the archive.

## OpenWrt official 25.12 package

The official website provides `remote-agent` packages built with the official OpenWrt 25.12.5 SDK (apk-tools, `0.1.29-r1` package version, including agent 0.1.29):

| target | package architecture | files |
|---|---|---|
| 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` |

LuCI interface is common: `luci-app-remote-openwrt-official.apk`. After checking the architecture with `apk --print-arch` `apk add --allow-untrusted ./文件.apk`. The MIPS package additionally depends on `libgcc1` (the official image comes with it by default).

## Linux musl (Alpine) and arm64

- `remote-linux-arm64.tar.gz`: aarch64, glibc 2.28+ (Debian 10+/Ubuntu 18.04+/RHEL 8+, Raspberry Pi 64-bit system, etc.), supports `remote-agent install` registration with systemd and self-update.
- `remote-linux-musl-x64.tar.gz` / `remote-linux-musl-arm64.tar.gz`: statically linked musl for distributions without glibc such as Alpine. Alpine has no systemd by default; use `remote-agent install --no-start` to write the configuration, then add your own OpenRC service (`/etc/init.d/remote-agent`) or run it in the foreground. musl builds support `update self` and desktop auto-update and only install the musl archive; after a one-shot update they try systemd, then OpenRC (`rc-service remote-agent restart`), then a process restart. A musl archive on an OpenWrt host still updates through opkg/apk.

## Linux

It is recommended to use the built-in subcommand (which will copy the current executable file, write private configuration and register systemd; does not rely on the `install/` files in the repository):

```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 同时删二进制与配置
```

Equivalent manual steps (still available):

```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
```

The user service runs by default with the login session. When booting and resident is required, the administrator can execute `loginctl enable-linger <user>`, or use the above `--system` system unit (`RuntimeDirectory=remote-agent`, `WantedBy=multi-user.target`). Remote commands use service account permissions. Stopping the service terminates all processes within the service. `install/linux/install.sh` is a thin wrapper around `remote-agent install --force`.

## Windows

After installing the Rust MSVC toolchain and 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` copies the binary and configuration to `%LOCALAPPDATA%\remote-agent`, restricts the profile ACL, and registers the scheduled task `RemoteAgent` that starts on login. By default, if the task or target file already exists, an error will be reported; `--force` will first stop and unregister the old task and then overwrite it. This mode requires user login; it is not a Windows SCM system service. `install/windows/install.ps1` is a thin wrapper around `remote-agent install` (`-Force` can be added).

If you use the UI to generate the configuration first, `--config` can point to a separate path (such as the file generated by `--ui --config .\device-config.json`).

Windows commands use PowerShell syntax, such as `Get-Process`, and the output is sent in UTF-8. Each execution session and terminal has an independent Job Object; its child process tree is cleaned up on timeout, cancellation, disconnection, or agent exit.

## macOS

Requires Rust toolchain (`rustup`, with default `aarch64-apple-darwin` or `x86_64-apple-darwin` target). The indicators are collected by sysctl, `getloadavg(3)` and mach `host_statistics`, and the static information (model, kernel, system version) is collected by `sysctl` and `sw_vers`; the platform reports it as `macos`.

On-demand sensors (non-heartbeat) do their best to collect without sudo or additional SMC dependencies: `pmset -g batt` battery (laptop), `pmset -g therm`’s CPU_Speed_Limit / Scheduler / Available CPUs and thermal pressure text, `ioreg`’s AppleSmartBattery `Temperature` and fans RPM (if firmware exposed). Desktop computers (such as Mac mini) often have no battery; Apple Silicon may also have no numerical CPU_Speed_Limit. Raw CPU die/proximity temperature requires private IOKit/SMC, currently not scraped. If thermal pressure, throttling, or fan readings are available, the Agent does not mark `thermal` as unavailable. After enabling `desktop-ui`, you can use `remote-agent --ui` to configure and switch login auto-launch (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` Copy the current executable file to `~/.local/bin/remote-agent`, write `~/.config/remote-agent/config.json` (permission 600), generate `~/Library/LaunchAgents/us.ivjn.remote-agent.plist`, and execute `codesign -s - --force` on the binary (macOS will kill unsigned in-place replacement), and then `launchctl bootstrap` / `kickstart`. `RunAtLoad` + `KeepAlive` allows the agent to start immediately after logging in, or be pulled up by launchd after crash or disconnection. The log is written to `~/Library/Logs/remote-agent.log`. `install/macos/install.sh` is a thin wrapper. `$HOME/.local/state/remote-agent/agent.state` is used when no status file is specified. Remote commands run with the permissions of that logged-in user.

Stop/Uninstall:

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

When it is necessary to boot up (when no one is logged in) and stay there, use LaunchDaemon under `/Library/LaunchDaemons` instead and load it as root; in this mode, remote commands run with root privileges, please evaluate the risk accordingly.

## Android Standalone APK

The device-side project is at `android-agent/`. `mobile/` is still an operation and maintenance client. The two can be installed separately without Expo Go, Termux or root.

The default address of Agent for all platforms is `wss://exchange.ivjn.us`. Android can click **Connect with a six-digit code**, and the administrator CLI will enter the six-digit code for authorization and automatically obtain the device identity; the native Agent (Windows/macOS local page, OpenWrt LuCI "Add Device", command line `remote-agent pair` / `remote-agent join <邀请>`) can display the six-digit code and QR code for Console to scan and approve, or paste a one-time invitation to join. See [Initialization and Pairing](/docs/en/pairing-and-enrollment.md) for details.

### First connection and home screen

1. Open **Remote Agent** on the Android device you want to manage. For first setup, choose **Connect with a six-digit code** or **Scan connection QR code** and obtain administrator approval. See [Initialization and pairing](/docs/en/pairing-and-enrollment.md) for account, QR-code and six-digit-code workflows.
2. If you already have a device ID and device token, open **Settings → Connection settings**, enter the server and credentials, and select **Save and connect**. The credential fields are hidden while the connection is enabled. Disconnect from the home screen before changing the device identity.
3. The home screen shows connection status, screen-sharing controls and the manager list. Select **Connect device** when configuration is saved but the connection is disabled. Reopening the app restores an enabled connection.

Android 12 and later use the system splash screen; Android 8–11 use a launch background with matching brand colors. The app opens the home screen when its first frame is ready, without waiting for a network connection or adding a minimum splash duration. Transitions are skipped when system animations are disabled. Settings load on demand; opening the home screen grants no accessibility or screen-sharing permissions. This describes current source behavior; check the installed version before expecting it in a public download.

### Background operation, notifications and stopping

Moving the app to the background or closing its interface does not stop the foreground service. The Agent reconnects after network loss. With the connection enabled, device boot and unlock, or an app package update, trigger an attempt to restore the service. After a force-stop, reopen the app manually. Manufacturer battery policies may also require permission to run in the background.

When notifications are allowed, select **Manage connection** to return to the app. While sharing, the notification offers **Stop sharing**, which ends screen sharing only. To disconnect remote access and disable automatic restoration, select **Disconnect** on the home screen and confirm. Saved configuration remains available for **Connect device** later. The home-screen **Stop screen sharing** action also ends sharing without disconnecting remote access.

The device token is encrypted with Android Keystore AES-GCM. App configuration is excluded from backup, and exported Activities or Services do not pass tokens. Remote commands run under the APK application UID, constrained by Android sandboxing and SELinux. This grants neither access to other apps' data nor root privileges; system settings, accessibility and screen sharing require their own authorization.

While remote access is enabled, the foreground service holds CPU and screen wake locks, keeping the screen on in the background and during reconnection at the cost of extra power. Disconnecting or destroying the service releases the locks and restores normal system screen timeout behavior. The power button still locks the device normally and does not bypass authentication. Notifications show connection or sharing status; the home screen provides the disconnect action.

### Build from source

Start each Debug or Release example below from the repository root. Check the device list before installing and replace DEVICE_SERIAL with the intended device serial. Packaging, unit tests and full Lint are separate checks. If Lint fails, inspect the report and fix or document the cause; skipping the task does not count as passing it.

Build requires JDK 17+, Android SDK Platform 36, NDK `28.2.13676358`, Python 3, and Rust. Set `ANDROID_HOME` (or `ANDROID_SDK_ROOT`); custom NDK paths use `ANDROID_NDK_HOME`. Full-screen display of `.Aran` depends on Metafile Android SDK: place the `Metafile` repository in the same directory as `remote-control` (Gradle references the `:aran` module through `../../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 automatically calls `scripts/build-android-agent.py` to load the two ABI Rust executable files into the APK native library directory. After installation, start directly from this read-only directory, which complies with Android's restrictions on the application data directory prohibiting the execution of files. The debug APK is used for development verification; official distribution must use the organization's own signature configuration to build the release, and the signature key is kept to support subsequent upgrades.

Release builds read `android-agent/keystore.properties` (which has been ignored by Git), or the external configuration pointed to by the environment variable `REMOTE_AGENT_SIGNING_PROPERTIES`. Fields see `android-agent/keystore.properties.example`; keys should be kept outside the repository. Release builds will fail when signature configuration is missing and will not fall back to Debug signing.

```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
```

Please securely back up the release key and its password. Subsequent versions must use the same key to cover upgrades. Debug and Release signatures are different. Existing Debug installations cannot be directly overwritten; uninstallation will clear the local configuration and device credentials, and re-pairing should be arranged first. `scripts/build-site-downloads.py` uses the signed Release APK as the Agent download package. Release signing does not guarantee that Play Protect will allow the APK.

### Android scan code to connect

**Scan connection QR code** on the Remote Agent homepage uses the phone camera to identify the connection QR code offline without Google Play services. Allow camera permissions when using for the first time; canceling the QR code scan will not modify the configuration. After successfully scanning the QR code, the server and device ID will be displayed, and the token will remain hidden. Click **Connect** before encrypting, saving the configuration and starting the connection; if an existing connection is running, it must be stopped before new configuration can be imported. Scanning the QR code does not automatically turn on screen controls or accessibility features.

First register an independent device in Exchange and obtain its **device token**, and then generate a QR code locally on your computer (do not use an operation and maintenance account token or a third-party online QR code website):

```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 从标准输入读取。
```

Open the generated SVG on your computer and let the Remote Agent scan it. The QR code file contains the device token and should be kept private and deleted after use and not submitted to Git. The tool refuses to overwrite existing files.

The QR code content is UTF-8 JSON, with a total length of up to 2048 bytes:

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

Scanning the QR code only accepts the `wss://` address, which can include the `/ws/device` path, and does not accept URL user information, query parameters or fragments. `server`, `device_id`, and `token` must be non-empty strings, with a maximum of 512, 128, and 512 characters respectively, and cannot contain control characters or leading and trailing blanks. Manual configuration is still available for the local `ws://` test environment.

### Android Accessibility, Screenshots and Text Input

Use `remote-cli` for the following commands. The device reports the `android_control` capability through the APK bridge; check `status` for granted permissions.

1. Connect the device, then open **Settings → Screen and interaction** and enable **Allow remote screen controls** (off by default).
2. Select **Accessibility settings** and enable Remote Agent in Android settings. This allows reading accessible UI trees, tapping, swiping, system navigation and replacing text in the focused input field.
3. Android 11+ can capture screenshots through enabled accessibility support. On Android 8–10, or for screen capture alone, select **Share screen** on the home screen or **Standard screen sharing** in settings and confirm Android's screen-sharing dialog. Authorization must be renewed after the sharing session ends or the process restarts.

```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` Use Android `ACTION_SET_TEXT` **Replace all text in the currently focused input box**, supports Chinese, emoticons and multi-line, up to 8192 UTF-8 bytes, NUL is not allowed. Click the editable input box first; controls that are not focused, cannot be edited, or do not support this action will return an error. `--stdin` reads UTF-8 and preserves newlines, also avoiding putting text into the command line history. `key` supports `back`, `home`, `recents`, `notifications`, `quick-settings`.

`xml` returns the nested `<hierarchy><node ...>` interface tree, including `resource-id`, `text`, `content-desc`, `bounds` and control status; up to 200 nodes, 50 layers, and up to 256 attributes characters, both the response and the root element will indicate whether to truncate. `click` is an exact match using the full values ​​of these properties (not XPath): `--view-id` corresponds to `resource-id`, `--description` corresponds to `content-desc`; multiple conditions are combined by AND. Only visible and enabled controls are matched. Zero or multiple matches result in an error; the click will not be executed if the traversal exceeds the limit. The text label will look for a clickable parent control. If it cannot be clicked, use `tap X Y`. XML should be re-obtained after interface changes, and long attributes can be positioned using resource IDs instead.

For input-method entry, open **Settings → Remote Input**. Select **Enable Remote Input** to enable Remote Input in Android, then **Select input method** to select it. After focusing on the target application input box, `type` inserts text at the cursor/selection via `InputConnection.commitText`, supports Chinese, Emoji and multi-line, and uses the same 8192 UTF-8 byte limit as `text`. It will not automatically click to send or submit the form, nor can it modify the Agent's own configuration. **Switch keyboard** on the keyboard restores your daily input method. `status` returns `ime_enabled`, `ime_selected` and `ime_connected`. The screen must be turned on and the device must be unlocked before reading and clicking on the interface or entering text.

The **Device Screen** page of Remote Console provides coordinate clicks, XML acquisition and attribute clicks, and Remote Input text input. Unavailable permissions or screen status will disable the button; successful input will clear the text, and the screenshot, XML and content to be input will be cleared when the page enters the background.

### System capabilities

System settings work according to the provider reported by the device, and the namespace is a string, not a hard-coded enumeration. Android still uses capability `android_settings`, provided by `system`, `secure`, `global`. Linux (without OpenWrt) and macOS usage capabilities `system_settings`: gsettings for the current user on Linux, `/usr/bin/defaults` for the current user on macOS. The browser extension uses the ability `browser_settings` to read and write `privacy.network`, `privacy.services`, and `privacy.websites` through an encrypted session. Windows and OpenWrt do not provide this set of settings. When adding a new key value setting, just register the provider in the Agent or extension, and the operator will render according to the list.

Android's three namespaces have nothing to do with screen switches, displays, and lock screen states, and are operated through ContentResolver rather than shell splicing. To write `system` settings from a regular APK, you need to click **Allow to modify system settings** on the Agent and authorize it; to write `secure/global`, you need additional `WRITE_SECURE_SETTINGS` system permissions. The status returns the current permissions and `providers`, and Android and manufacturer policies can still deny individual reads and writes.

`settings list --prefix` Filters setting names on Android; on Linux/macOS, without prefix only returns the schema or domain name, with prefix returns the top-level string, integer and Boolean value of this schema or domain. `settings clear` Currently only browser privacy entries will be accepted, other providers will return explicit errors. The audit records the namespace, setting name, and number of characters, but not the value.

```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
```

The value of `settings set` is a string, and `--stdin` retains newlines; the set value is not written to the audit. Values ​​can be up to 8192 UTF-8 bytes, NUL is not allowed; setting names can be up to 256 ASCII characters (letters, numbers, underscore, dot, colon, dash). The list has a maximum of 500 items and 512 KiB. If the limit is exceeded, `truncated` is returned; the maximum single value is 64 KiB. Non-existing settings return `value: null`. The interface will not silently force modification of restricted settings. It will be read again for confirmation after each write, and a clear error will be returned if it fails. The modification will take effect permanently, and the old value will not be restored when the Agent is disconnected. The `screen-timeout` shortcut command uses seconds (15–86400), and the underlying `screen_off_timeout` uses milliseconds; automatic screen-off and the delay before locking after screen-off are separate settings.

### Media playback (media)

Reading, audio, pictures (including full-screen text), videos and web pages are unified with the `media` capability, and are notified on Android and Windows/Linux/macOS Agents; OpenWrt is not notified. The protocol has only three actions: `media_play` (with a `media` object), `media_stop`, and `media_status`, all three of which return the same `media` state.

| `media.type` | Field | Channel |
| --- | --- | --- |
| `speech` | `text`, optional `language` (BCP-47, such as `zh-CN`) | Sound |
| `audio` | `url` or `path` Choose one, optional `loop` | Sound |
| `image` | Choose one of `url`, `path`, `text`, optional `p2p` (`text` cannot be brought) | Screen |
| `video` | `url` or `path` Choose one, optional `p2p` | Screen |
| `web` | `url` (https only) | Screen |

There are two independent channels on the device: **Sound** (reading, audio) and **Screen** (pictures, text, videos, web pages). New content only replaces the one currently playing in the same channel, so background music can be played at the same time as the image carousel. `channel` of `media_stop` can be `sound`, `screen`, or `all` (default).

```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` returns:

```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` is the latest item in the sound channel (retained after the end for easy viewing of results), and the status is `queued` / `speaking` / `loading` / `playing` / `completed` / `stopped` / `error`; `null` when never played. `screen` only appears when the screen is being displayed, otherwise it is `null`. `speech_available=false` means there is no speech engine available on the device. Media volume only has a value on Android, and is 0 on desktop.

Verification and auditing: Text (reading or picture text) can be up to 4000 UTF-8 bytes, and NUL is not allowed; the URL must be `https`, up to 2048 bytes, and cannot contain blanks or control characters; the path can be up to 4096 bytes, and cannot contain control characters. The audit only records the type, source category (`url` / `path` / `text`), number of characters, whether to loop and whether to use P2P, never the URL, path or body (URLs may have a signing token). The `readonly` account cannot invoke any media operations.

#### Sound: Reading and Audio

- Speak aloud: Android uses the system TTS engine; Windows uses `System.Speech`; macOS uses `say`; Linux uses `spd-say` / `espeak-ng` / `espeak` (when `speech_available=false` is not installed, Speak aloud returns a clear error). The desktop will try its best to map language tags to local speech. Android does not return until the engine starts making sounds; returns an error when the system engine is initialized or language data is unavailable; stopping the Agent releases the TTS.
- Audio: Source is an absolute path on the device or an https URL. For files that are not on the device, use `remote-cli files upload` to upload them first and then play them according to the path. `loop` Loops until stopped.
  - Android uses `MediaPlayer` with the media volume channel and streams URLs directly. It will not return until the sound starts. If it fails to start within 10 seconds, an error will be returned.
  - The desktop side first downloads the URL to the current user's temporary state directory (maximum 50 MB, redirection only allows https), and will delete it after playing, stopping, or errors; the interface will return after the playback process is started, and failure to download or decode will be reflected by `media status` and `sound.state=error`. Use `MediaPlayer` for Windows (supports mp3, wav, m4a, aac, wma, flac); use `afplay` for macOS; search in sequence for Linux `mpv`, `ffplay`, `paplay`, `pw-play`, `aplay`, an error is returned if none are installed (`paplay` / `aplay` does not support mp3).
  - The path must be an existing and readable ordinary file; Windows rejects network paths like `\\server\share` to avoid sending credentials to the remote SMB server.
- No sound may be heard when the device volume is zero.

#### Screen: pictures, text, videos and web pages

- **image**: Native `path`, `https` URL, or `text` (large UTF-8 text on dark background, treated as a static image). Sources with the extension `.Aran` on Android will be decrypted using Metafile Android SDK before being displayed.
- **video**: The local `path` or `https` URL, played in a loop. `.Aran` on Android cannot be used as a normal HTTPS media stream: when it is not cached, it is decrypted in blocks and played while downloading, and the plain text is written to the local cache for direct playback next time. The HTTP block address is `/meta/<SHA1>` in the origin root path; native files read the `meta/` directory next to the manifest. Defaults to using either the manifest-embedded key or the SDK default key. Optional P2P: The block source sequence is local `meta/`, UDP P2P (`LOOKUP_ONLY`, not registered in metastore), HTTP fallback. The discovery service and static peer are provided by `media.p2p`, such as `--p2p-group library --p2p-discovery https://p2p.ivjn.us`; when P2P startup fails, HTTP/native is still used and playback is not interrupted.
- **web**: Open a `https` page in full screen. Android uses WebView in a full-screen Activity (enable JavaScript and DOM storage, and navigation within the page only follows https; when the rendering process crashes, the display is turned off without affecting the Agent). The desktop starts an independent browser process in kiosk mode, uses a temporary configuration directory, and ends it together with the browser when stopped: Windows looks for Edge, Chrome, and Firefox in sequence; macOS looks for Chrome, Edge, Chromium, and Firefox (Safari does not have kiosk mode); Linux looks for Chromium, Chrome, Edge, and Firefox. Returns an explicit error when the browser is not found

Android automatically uses another domain name as an alternate source for the `.Aran` manifest and data block for `https://cdn.ivjn.cn` and `https://cdn.ivjn.us`, retaining the manifest path, and the data block still reads `/meta/<SHA1>` from each source. Other domain names will not be automatically mapped. When a temporary error (including HTTP 554), timeout or data block verification fails, the alternate source will be tried first, and then backed off by 250/500 ms for up to 3 rounds; this source will be used first after success. Each connection timeout is 5 seconds, and the read timeout is 30 seconds; permanent errors such as 403/404 do not request the same source repeatedly in the same read. Block size and SHA-1 are still verified, redirects are not followed, and HTTPS verification is not relaxed. This behavior requires Metafile `ab2b92f` or a newer version of the Android SDK that contains `MirroredHttpSource`.

Picture channels display only one item at a time; new pictures replace the previous item. Keep screen on during presentation (Android: `FLAG_KEEP_SCREEN_ON` + `turnScreenOn` / `showWhenLocked`; Windows: `SetThreadExecutionState`; macOS: `caffeinate -d`; Linux: `systemd-inhibit` or `xdg-screensaver inhibit`). Static images and text slowly pixel drift to prevent screen burn; videos only loop; web pages do not drift. No static highlight borders. Android via standalone full-screen activity (no accessibility required). The Linux screen requires an unlocked X11 session, and the video requires `mpv` or `ffplay` installed on the machine; macOS video prefers mpv, otherwise `open`.

#### Control: Progress and Web Pages

`media_control` controls the content being played by channel and returns to the same `media` state. `channel` must be `sound` or `screen`; `op` takes one of the tables:

| `op.type` | Parameters | Applicable |
| --- | --- | --- |
| `pause` / `resume` | — | Audio (sound), video (screen) |
| `seek` | `position_ms` (absolute position, up to 7 days) | Audio, video |
| `navigate` | `url` (https) | Web page |
| `reload` / `back` / `forward` | — | Web page |
| `scroll` | `dx`, `dy` (pixels, non-zero, each not exceeding 100000) | Web page |
| `eval` | `script` (up to 16 KiB) | Web page, returning `script_result` (JSON encoding of the result, up to 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"
```

Added in the status: `position_ms`, `duration_ms`, `paused` of `sound` / `screen` (audio and video, when the player can report), `url` of `screen` on the web page with `title`. Reading, pictures and text cannot be controlled; a clear error will be returned when the content in the channel does not match the operation. The audit only records the channel, operation type, and script character count, not the URL or script content.

- Android: Audio and video are directly controlled by `MediaPlayer` / `VideoView`, and the web page controls the WebView within the full-screen Activity.
- Windows: Audio is executed by the built-in `MediaPlayer` worker; video is controlled by IPC pipe under mpv, WPF fallback player can also be controlled, ffplay does not support it.
- Linux / macOS: Audio and video can only be controlled when the player is mpv (JSON IPC), otherwise it will return "Install mpv to pause or seek desktop media"; when macOS is equipped with mpv, the audio will also be played using mpv instead.
- Desktop web page: Chrome / Edge / Chromium's kiosk is started with `--remote-debugging-port=0` (only bound to 127.0.0.1, random port, temporary configuration directory), executed through the DevTools protocol; Firefox only supports `navigate` (restart the kiosk with a new address).

The Remote Console mobile phone provides "System Settings" and "Media" tabs on the **System Capabilities** page of the device details; the media page can read the status of two channels, stop the sound or picture respectively, play readings, audio URLs, text, pictures, videos and web pages, and control the progress of audio and video and web pages (jump, forward and backward, refresh, scroll, execute JS). When the page enters the background, the setting results, the setting values ​​to be sent, the text read aloud, and the input display will be cleared. The web console provides the same setting form, and the browser settings reuse the established encrypted session; there is no separate media playback page on the web page.

### Android text search and XML

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

Text search matches both the text and description of visible nodes. It uses substring matching by default, with optional exact matching and case-insensitive matching. Returns each matching `bounds` and `center` (original screen pixels, clickable with `tap X Y`), resource ID, text, and enabled status. The center point is calculated from the intersection with the screen to avoid returning click coordinates outside the screen; the result is to traverse up to 200 nodes and report truncation. Password nodes and the Agent's own configuration subtree do not participate in the search. Self-drawn text may not be read by Android accessibility features, and this interface does not perform OCR. You should search again after the interface changes.

Screenshots are JPEG, with a maximum length of 1600 pixels on the longest side, and the CLI refuses to overwrite existing files. The CLI displays images simultaneously with the original screen size; clicks, swipes, and tree nodes `bounds` use the original screen pixels. When positioning based on thumbnails, use `屏幕坐标 = 图像坐标 × screen_width / width` (use height scale vertically). Reread dimensions after rotation. The interface tree has a maximum of 200 nodes, and each string has a maximum of 256 UTF-16 units. `truncated` indicates that the number of nodes is limited. The self-drawn interface of the application may not have accessible nodes.

The text and description of the password node and the Remote Agent's own configuration subtree will not be returned; remote text input cannot modify its own configuration. Windows protected by Android `FLAG_SECURE` are blacked out or return system errors in screenshots; Android 15's screen sharing may also automatically block apps containing sensitive content such as password fields. Screen control reuses existing operation and maintenance authentication, and the `readonly` account cannot initiate these operations. The audit only records actions, coordinates and the number of input characters, and does not save the input text, interface tree or screenshots.

**Stop screen sharing** Ends the MediaProjection sharing session; if Android 11+ accessibility is still enabled, accessibility screenshots will still be available. Closing **Allow remote screen controls** will prevent all subsequent screen operations and end sharing; **Stop remote access** will also disconnect the agent. Clicks or inputs that have already been performed cannot be undone. Accessibility permissions are managed separately by Android settings; system authorization is retained when the switch is turned off or the service is stopped, but remote screen operations are not accepted.

The local bridge uses a randomly named Unix socket and verifies that the caller has the same UID as the APK. This prevents access by other ordinary applications; the authorized remote shell shares the application UID with the APK, and the screen switch should not be treated as a permission isolation for that shell or the device administrator.

## System information and indicators

`DeviceInfo.platform` is `linux`, `freebsd`, `netbsd`, `windows`, `android`, `macos` or `openwrt`, `os_version` are general system versions, `capabilities` describes `exec`, `pty`, `ubus`, `android_control`. OpenWrt reports `openwrt_version`.

Linux/OpenWrt collects metrics from `/proc`; Android retains other readable metrics when it encounters SELinux limitations, and the running time can be obtained through `CLOCK_BOOTTIME`. Windows collects memory, CPU, elapsed time, and network card counts through native system APIs. Indicators that cannot be collected are marked in the `unavailable` list, and the CLI and mobile terminals display missing items. Windows does not have a Unix load average, so `load` is always marked as unavailable.

## Verify

```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` is a local test relay, using Python `websockets`, to verify the registration, indicators, execution, timeout, cancellation, PTY, resize and unsupported ubus of the real agent. It does not replace Exchange authentication and database testing. Use fixed test credentials as instructed by the script and do not connect to official devices or services. Both Windows and Linux should run their own native tests; Android should also verify the foreground, background, stop and reconnect in the actual running environment after installing the APK.

`scripts/agent-ui-smoke.py --agent <binary> --platform linux|windows` separately verifies the first configuration of the UI, authentication and source restrictions, private file storage, token retention, actual registration and execution, cleanup on stop, reconnection after disconnection, and reopening. Requires Python 3.11+ and `websockets`; the agent to be tested must be built with `desktop-ui` first. Browser validation should also cover save buttons, input errors, token visibility, refresh recovery, and narrow screen layouts.

`scripts/android-control-smoke.py` Verifies screen operation using real Exchange, CLI, and standalone Android test apps. Run only in dedicated, disposable emulators: The script clears Remote Agent application data and modifies accessibility settings. First build and install the two APKs `:app:assembleDebug` and `:control-fixture:assembleDebug`, prepare an independent Exchange/database, register the test device and save the device token and CLI configuration; Python 3.11+, Pillow and `adb` are required.

```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
```

Windows changes the CLI path to `target/debug/remote-cli.exe`. The test product is located at `target/android-control-qa/screenshots/` by default. The fixture is only used for testing and does not enter the device APK.


### Android logs, display status and wake

Remote Console adds **Device logs** and **Device screen** to device details. The screen page shows display presence, power state, lock state, dimensions and permissions, and provides an on-demand JPEG preview and **Wake screen**. Devices without a display or without the Android control interface have disabled screen actions; missing capture permission is shown separately. Screen-off devices can be woken when local screen controls are enabled. Wake never dismisses the keyguard.

The Android Agent includes **Screen status and device logs** locally. While remote access is enabled, connection fields, QR enrollment and connection-code buttons are hidden. Disconnect requires confirmation and preserves saved credentials. Accessibility authorization and an actively bound service are reported separately; if authorized but waiting for binding, toggle only Remote Agent in system Accessibility settings.

```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
```

The existing `open_android` WebSocket interface accepts `{action:"logs",limit:100}` (1-300 entries) and `{action:"wake"}`. Status adds optional `display_present`, `screen_on`, `locked`, `screen_width`, `screen_height` and `accessibility_authorized`; `accessibility_enabled` continues to mean the service is connected. Android native-screen APIs still require the APK, and non-Android agents do not advertise this interface.

Logs now come from `logcat -b all`, without PID, package or tag filtering: system, application, events and crash buffers available to Android logd. Each snapshot returns up to 100 entries in the UI or 1-300 with the CLI, retaining timestamps, levels, buffer names, PID/TID, tags and original messages. This is the device's current logcat history, not an archive of arbitrary log files or logs Android has already rotated out.

Grant the device APK system-log permission through an authorized ADB connection:

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

On Android versions requiring per-use log access, open the Agent diagnostics page and accept the system confirmation, then retry the remote request. Background access may be denied or approval may expire according to Android/OEM policy. Android can silently return only the caller's UID after denial, so the collector examines UID metadata from the latest 301 logcat entries before selecting the requested tail. If no other UID appears (including empty logs), it reports that full-device access could not be confirmed instead of presenting an app-only snapshot. Missing permission or logcat failures also return an explicit error; the collector does not fall back to the Agent event ring. Collection runs off the UI thread, stops on cancellation, and has a six-second deadline and bounded output; oversized snapshots report truncation or an error. Logs use existing authenticated operator sessions, tenant routing and role restrictions. Device log text may contain content written by other apps. The web and mobile log views clear on page exit or backgrounding and do not save snapshots to disk. Remote Console also clears screenshots on exit/backgrounding.


**Transparent full-screen sharing:** In Android Agent choose **Share full screen with transparent overlay**. Grant Android's display-over-other-apps permission and screen-sharing consent. The Agent then moves to the background and installs a full-screen `TYPE_APPLICATION_OVERLAY` with zero opacity and `FLAG_NOT_TOUCHABLE | FLAG_NOT_FOCUSABLE`: all touches go to the underlying application. MediaProjection captures the full display (Android 14+ explicitly requests the default display). Stop sharing from the Agent page or notification; stopping sharing does not disconnect the Agent. The overlay is removed when sharing ends or the service stops.

Screen preview and connected Agent status pages allow capture. Connection setup, login tickets and token QR pages remain protected. Android may still return a black image for another app's secure window; a transparent overlay cannot bypass that app's protection.

Before capture the Android Agent checks both display state and PowerManager interactive state. It wakes only an off display, verifies that it became interactive, and refuses capture while keyguard remains locked. The MediaProjection reader continuously drains frames and retains one current composed image instead of recreating its surface for every screenshot. Entirely black frames wait for a usable frame and produce an actionable error on timeout rather than a successful black JPEG.

## Agent mode comparison

Launch mode and device platform are separate dimensions. Native headless, local UI and system services share the connection core; `desktop-ui` only adds local configuration and does not grant remote permissions.

| Mode/platform | Capabilities | Conditions and boundaries |
|---|---|---|
| headless / local UI | Same native protocol, commands, terminal, files, metrics, notifications, optional LAN | UI on Linux/Windows/macOS only; Windows tray; desktop actions require a usable graphical user session |
| System service / login startup | Same native Agent | Inherits service account and environment; launch overrides are not copied to another service |
| OpenWrt | Native core, ubus | Advertises ubus when available; supervised by procd; no desktop-control capability |
| Linux / Windows / macOS | Native core, desktop control, system logs, media | Requires OS permissions/tools; system settings advertised only on Linux/macOS |
| FreeBSD / NetBSD | Native core, terminal, files, network, metrics, optional LAN | Battery unavailable; X11 dependencies required; no media/system settings |
| Android APK | Rust core + Java bridge, screen, settings, media, OTA | App UID and OS permissions; standalone Rust binary has no APK control bridge |
| Chromium extension | URL, cookies, pages, browser settings, CDP, metrics | No OS commands/PTY/arbitrary host files/LAN listener; full CDP requires local permission |

UI and headless share runtime configuration resolution: CLI/environment override JSON for LAN, watchdog, TLS and notifications; auto-update retains CLI > JSON > `REMOTE_AUTO_UPDATE` > enabled by default. `REMOTE_WATCHDOG=1` and 0, true/false, yes/no, on/off are accepted. UI still rejects launch overrides for connection credentials and state paths; runtime overrides are not saved to JSON, and autostart processes need separate configuration. LAN status shows effective values; saving the page does not remove launch overrides.

UI and headless use JSON `state_file`, otherwise the same persistent system default; separate device instances need separate paths. Connecting refreshes the status and stopping shows stopped. Stopping cancels that connection's auto-update future and releases watchdog state; workers exit on their next check, and completed operations cannot be undone. Each reconnect also reloads device identity and trust policy; a failed identity load clears the previous identity instead of retaining the old device authorization.

All device operations require sealed sessions and workspace-root authorization. Full browser CDP's per-call consent/unattended permission is not isolated by typed cookie scopes and cannot bypass Chrome policies. Extension closure or disconnect requires a new handshake. Desktop update archives map to Windows x64, Linux glibc x64/arm64 and macOS arm64; other native platforms use installers/package managers, Android uses APK OTA, and browsers use extension packages. Advertising a capability does not guarantee hardware, permissions or a published archive.

## Automatic screensaver

Local Agent and browser extension settings support idle (300 seconds by default), control-start/end and immediate entry. Choose text, HTTPS images/videos or isolated static HTML. Videos loop muted; text and images can travel across a wide area. Desktop and Android offer an exit password; browser fullscreen windows have no password and exit on a key, pointer movement or window close. Only the active display is covered; this is not an OS lock. Linux requires X11 and xprintidle. Headless OpenWrt, Wayland and BSD do not currently support screen display.

Solid color mode displays only the chosen background, without text or media. Background color uses #RRGGBB; brightness ranges from 0 to 100% (black at 0%). Android also dims the screensaver window backlight and restores system brightness on input. Other platforms dim the image only; LCD power savings depend on backlight control. Exit prompts remain readable.

From 0.1.34, motion moves only text and images across a wide area on the background. Large content scales down when needed to at most 44% of the available width and height, so opposite corner positions do not overlap. HTML, video and solid color stay still; reduced-motion preferences pause travel. Every mode hides the pointer from the first frame, restores it for the exit-password dialog and hides it again on cancel. Windows refreshes the pointer inside the screensaver window every 100 ms without changing the system cursor. These changes ship first in Windows and Browser Agent 0.1.34 packages; check the download manifest for other platforms.
