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:
@@ -2,13 +2,7 @@ import { cancel, confirm, intro, isCancel, log, note, outro, spinner } from "@cl
|
||||
import { Command, Option } from "commander";
|
||||
import { writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import {
|
||||
generateLocalPairingOffer,
|
||||
loadConfig,
|
||||
loadPersistedConfig,
|
||||
type CliConfigOverrides,
|
||||
type PersistedConfig,
|
||||
} from "@getpaseo/server";
|
||||
import { loadPersistedConfig, type PersistedConfig } from "@getpaseo/server";
|
||||
import {
|
||||
resolveLocalPaseoHome,
|
||||
resolveLocalDaemonState,
|
||||
@@ -19,6 +13,11 @@ import {
|
||||
} from "./daemon/local-daemon.js";
|
||||
import { tryConnectToDaemon } from "../utils/client.js";
|
||||
import { formatPairingInstructions } from "../output/pairing.js";
|
||||
import {
|
||||
confirmRelayPairing,
|
||||
printDirectConnectionGuidance,
|
||||
resolveLocalPairingOffer,
|
||||
} from "./daemon/pair.js";
|
||||
|
||||
interface OnboardOptions extends DaemonStartOptions {
|
||||
timeout?: string;
|
||||
@@ -70,37 +69,6 @@ function parseTimeoutMs(raw: string | undefined): number {
|
||||
return Math.ceil(seconds * 1000);
|
||||
}
|
||||
|
||||
function toCliOverrides(options: OnboardOptions): CliConfigOverrides {
|
||||
const cliOverrides: CliConfigOverrides = {};
|
||||
|
||||
if (options.listen) {
|
||||
cliOverrides.listen = options.listen;
|
||||
} else if (options.port) {
|
||||
cliOverrides.listen = `127.0.0.1:${options.port}`;
|
||||
}
|
||||
|
||||
if (options.relay === false) {
|
||||
cliOverrides.relayEnabled = false;
|
||||
}
|
||||
|
||||
if (options.hostnames) {
|
||||
const raw = options.hostnames.trim();
|
||||
cliOverrides.hostnames =
|
||||
raw.toLowerCase() === "true"
|
||||
? true
|
||||
: raw
|
||||
.split(",")
|
||||
.map((host) => host.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
if (options.mcp === false) {
|
||||
cliOverrides.mcpEnabled = false;
|
||||
}
|
||||
|
||||
return cliOverrides;
|
||||
}
|
||||
|
||||
function savePersistedConfig(paseoHome: string, config: OnboardPersistedConfig): void {
|
||||
const configPath = path.join(paseoHome, "config.json");
|
||||
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
||||
@@ -338,6 +306,7 @@ export function onboardCommand(): Command {
|
||||
.option("--listen <listen>", "Listen target (host:port, port, or unix socket path)")
|
||||
.option("--port <port>", "Port to listen on (default: 6767)")
|
||||
.option("--home <path>", "Paseo home directory (default: ~/.paseo)")
|
||||
.option("--relay", "Enable relay connection without prompting")
|
||||
.option("--no-relay", "Disable relay connection")
|
||||
.option("--no-mcp", "Disable the Agent MCP HTTP endpoint")
|
||||
.option(
|
||||
@@ -477,8 +446,6 @@ export async function runOnboard(options: OnboardOptions): Promise<void> {
|
||||
}
|
||||
|
||||
const voiceEnabled = await resolveAndPersistVoice(paseoHome, options);
|
||||
const config = loadConfig(paseoHome, { cli: toCliOverrides(options) });
|
||||
|
||||
log.message(
|
||||
voiceEnabled
|
||||
? "Voice features enabled. Local speech models will be downloaded automatically if missing."
|
||||
@@ -492,26 +459,30 @@ export async function runOnboard(options: OnboardOptions): Promise<void> {
|
||||
richUi,
|
||||
});
|
||||
|
||||
if (config.relayEnabled === false) {
|
||||
log.warn("Relay is disabled; pairing offer is unavailable for this daemon.");
|
||||
if (options.relay === false) {
|
||||
log.message("Relay pairing skipped because --no-relay was provided.");
|
||||
printNextSteps(null, paseoHome, richUi);
|
||||
if (richUi) {
|
||||
outro("Paseo daemon is running.");
|
||||
}
|
||||
if (richUi) outro("Paseo daemon is running.");
|
||||
return;
|
||||
}
|
||||
|
||||
const pairing = await generateLocalPairingOffer({
|
||||
let pairing = await resolveLocalPairingOffer({
|
||||
paseoHome,
|
||||
relayEnabled: config.relayEnabled,
|
||||
relayEndpoint: config.relayEndpoint,
|
||||
relayPublicEndpoint: config.relayPublicEndpoint,
|
||||
relayUseTls: config.relayUseTls,
|
||||
relayPublicUseTls: config.relayPublicUseTls,
|
||||
appBaseUrl: config.appBaseUrl,
|
||||
includeQr: true,
|
||||
enableRelay: options.relay === true,
|
||||
});
|
||||
|
||||
if (!pairing.relayEnabled) {
|
||||
const shouldEnable = richUi ? await confirmRelayPairing() : false;
|
||||
if (!shouldEnable) {
|
||||
printDirectConnectionGuidance();
|
||||
printNextSteps(null, paseoHome, richUi);
|
||||
if (richUi) outro("Paseo daemon is running.");
|
||||
return;
|
||||
}
|
||||
pairing = await resolveLocalPairingOffer({ paseoHome, enableRelay: true });
|
||||
log.success("Relay enabled");
|
||||
}
|
||||
|
||||
if (!pairing.url) {
|
||||
log.warn("Relay pairing URL is unavailable for this daemon configuration.");
|
||||
printNextSteps(null, paseoHome, richUi);
|
||||
|
||||
Reference in New Issue
Block a user