Make relay opt-in when pairing devices (#2706)

* feat: make relay opt-in for device pairing

* fix(relay): keep pairing state authoritative

* fix(relay): refresh pairing state across transports

* fix(onboard): honor explicit relay opt-out

* fix(relay): surface transport startup failures

* fix(pairing): gate offers for older daemons

* test(cli): rely on real pairing transport coverage

* fix(cli): report live relay without a pid file

* fix(relay): synchronize config during daemon startup

* fix(relay): preserve mutability for ignored overrides

* fix(cli): use reachable daemon for pairing

* fix(app): redirect invalid pair device routes

* fix(pairing): support remote host device pairing

Keep Pair Device host-relative through the existing settings and daemon RPC flow. Route CLI output through an injected adapter so workflow tests do not mutate process globals.

* fix(pairing): validate pairing destinations

Expose the host-relative Pair Device flow on every supported app platform. Reject a reachable daemon whose identity does not match the requested Paseo home before reading or mutating its pairing state.

* fix(pairing): render QR codes across platforms

Generate SVG text without DOM or canvas and render it through the existing cross-platform SVG stack.

* fix(cli): pair from persisted relay state

Allow explicit relay pairing while the daemon is stopped when relay is already enabled in persisted configuration.
This commit is contained in:
Mohamed Boudra
2026-08-01 04:35:06 +08:00
committed by GitHub
parent 3778d0ad7b
commit d2de309fc3
73 changed files with 2163 additions and 543 deletions
+5 -1
View File
@@ -200,9 +200,13 @@ Use `PASEO_HOME` to run multiple isolated daemon instances.
Get an offer URL from the daemon you want to control:
```bash
paseo daemon pair --json # prints { url, qr, ... }
paseo daemon pair # asks before enabling relay, then prints the QR and link
paseo daemon pair --relay # enables relay without prompting
paseo daemon pair --json # structured output; never prompts
```
Relay is off for new installations. In non-interactive or JSON mode, a disabled relay returns a `RELAY_DISABLED` error; pass `--relay` to provide explicit consent. Relay pairing is end-to-end encrypted. See [Security](/docs/security).
Use it from anywhere:
```bash
+19
View File
@@ -187,11 +187,30 @@ A `password=` in the URI always wins over the env var, so you can keep `PASEO_PA
In the mobile app, enter the password in the direct connection setup screen.
## Relay
New homes write `daemon.relay.enabled: false`. Paseo asks before enabling relay when you pair a device; existing homes keep their saved value. See [Security](/docs/security) for relay encryption and direct connection alternatives.
Set the persisted value in `config.json`:
```json
{
"daemon": {
"relay": {
"enabled": true
}
}
}
```
`PASEO_RELAY_ENABLED=true|false` overrides the persisted value for that daemon launch. The matching `paseo daemon start --relay` and `--no-relay` flags have the same authority. Remove the launch override before changing relay from Paseo Desktop or `paseo daemon pair --relay`.
## Common env vars
- `PASEO_HOME`, set Paseo home directory
- `PASEO_PASSWORD`, on the daemon, the password to require (plaintext, hashed at startup); on the CLI, the password used to connect when the host URI doesn't include one
- `PASEO_LISTEN`, override `daemon.listen`
- `PASEO_RELAY_ENABLED`, enable or disable the outbound relay for this daemon launch
- `PASEO_HOSTNAMES`, override/extend `daemon.hostnames`
- `PASEO_ALLOWED_HOSTS`, deprecated alias for `PASEO_HOSTNAMES`
- `PASEO_WEB_UI_ENABLED`, enable or disable the daemon-served web UI
+2 -2
View File
@@ -14,7 +14,7 @@ Paseo runs your coding agents on your machine and gives you a mobile, desktop, w
Download from [paseo.sh/download](https://paseo.sh/download) or the [GitHub releases page](https://github.com/getpaseo/paseo/releases). Open it and you're done.
The desktop app bundles its own daemon and starts it automatically, no separate install required. On first launch you'll see a brief startup screen, then connect from your phone using **Settings → your host → Connections → Pair a device**.
The desktop app bundles its own daemon and starts it automatically, no separate install required. On first launch you'll see a brief startup screen, then connect from your phone using **Settings → your host → Pair Device**.
## Server / CLI
@@ -25,7 +25,7 @@ npm install -g @getpaseo/cli
paseo
```
Paseo prints a QR code in the terminal. Scan it from the mobile app, or enter the daemon address manually from another client.
Paseo starts the daemon locally, then asks whether to enable the end-to-end encrypted relay and print a pairing QR code. If you decline, enter the daemon address manually over TCP, Tailscale, or another VPN.
The daemon can also serve the browser web app itself, so you can use the full UI without the hosted app. See [Self-hosting the web UI](/docs/web-ui).
+2
View File
@@ -25,6 +25,8 @@ Clients connect to the daemon over WebSocket. There are two ways to establish th
The relay is the simplest way to connect from your phone. It requires no VPN setup, no port forwarding, and no firewall configuration. The daemon can stay bound to localhost or a socket file, it connects _outbound_ to the relay, and your phone meets it there. The official relay server is the open-source Elixir service at [getpaseo/paseo-relay](https://github.com/getpaseo/paseo-relay).
Relay is off on new installations. When you pair a device from `paseo`, `paseo daemon pair`, or Paseo Desktop, Paseo asks before enabling it. Choosing not to enable relay leaves the daemon available for direct TCP, Tailscale, or other VPN connections and does not create a pairing QR code. Use `--relay` with the CLI pairing or startup command to opt in without an interactive prompt.
> **The relay is designed to be untrusted.** All traffic between your phone and daemon is end-to-end encrypted. The relay server cannot read your messages, see your code, or modify traffic without detection. Even if the relay is compromised, your data remains protected.
### How it works
+1 -1
View File
@@ -76,7 +76,7 @@ By default the daemon listens on `127.0.0.1:6767`, reachable only from the same
paseo daemon start --web-ui --listen 0.0.0.0:6767
```
> **Anyone who can reach the listening address can use your agents.** Before you bind beyond localhost, set a password and review your host allowlist. The relay (the default mobile pairing path) avoids this entirely by keeping the daemon bound to localhost, see [Security](/docs/security).
> **Anyone who can reach the listening address can use your agents.** Before you bind beyond localhost, set a password and review your host allowlist. The relay pairing path avoids this entirely by keeping the daemon bound to localhost, see [Security](/docs/security).
Two things to configure when you expose the daemon directly: