Allow self-hosted relays to opt into wss:// (#767)

* Add relay TLS schema fields

* Wire relay TLS into websocket URLs

* Propagate relay TLS from pairing offers

* Expose relay TLS opt-in

* Mark relay TLS fallback for removal

* Preserve explicit relay TLS false

* Align relay TLS defaults outside config loader
This commit is contained in:
Mohamed Boudra
2026-05-06 17:25:16 +08:00
committed by GitHub
parent 6c319d05dd
commit 2e45650f22
33 changed files with 397 additions and 17 deletions
@@ -77,4 +77,23 @@ describe("local daemon launch supervision", () => {
expectSupervisorLaunch(argv);
expect(argv).toContain("--no-mcp");
});
test("relay TLS flag is passed to the supervised daemon", async () => {
mocks.spawnSync.mockReturnValue({ status: 0, error: undefined });
const { startLocalDaemonForeground } = await import("./local-daemon.js");
const status = startLocalDaemonForeground({
home: "/tmp/paseo-test",
relayUseTls: true,
});
expect(status).toBe(0);
const [, argv, options] = mocks.spawnSync.mock.calls[0] as [
string,
string[],
{ env?: NodeJS.ProcessEnv },
];
expect(argv).toContain("--relay-use-tls");
expect(options.env?.PASEO_RELAY_USE_TLS).toBe("true");
});
});
@@ -11,6 +11,7 @@ export interface DaemonStartOptions {
home?: string;
foreground?: boolean;
relay?: boolean;
relayUseTls?: boolean;
mcp?: boolean;
injectMcp?: boolean;
hostnames?: string;
@@ -28,6 +29,9 @@ export interface LocalDaemonPidInfo {
export interface LocalDaemonState {
home: string;
listen: string;
relayEnabled: boolean;
relayEndpoint: string;
relayUseTls: boolean;
logPath: string;
pidPath: string;
pidInfo: LocalDaemonPidInfo | null;
@@ -93,6 +97,9 @@ function buildRunnerArgs(options: DaemonStartOptions): string[] {
if (options.relay === false) {
args.push("--no-relay");
}
if (options.relayUseTls === true) {
args.push("--relay-use-tls");
}
if (options.mcp === false) {
args.push("--no-mcp");
@@ -117,6 +124,9 @@ function buildChildEnv(options: DaemonStartOptions): NodeJS.ProcessEnv {
if (options.hostnames) {
childEnv.PASEO_HOSTNAMES = options.hostnames;
}
if (options.relayUseTls === true) {
childEnv.PASEO_RELAY_USE_TLS = "true";
}
return childEnv;
}
@@ -353,6 +363,9 @@ export function resolveLocalDaemonState(options: { home?: string } = {}): LocalD
return {
home,
listen,
relayEnabled: config.relayEnabled ?? true,
relayEndpoint: config.relayPublicEndpoint ?? config.relayEndpoint ?? "relay.paseo.sh:443",
relayUseTls: config.relayUseTls ?? false,
logPath,
pidPath,
pidInfo,
+1
View File
@@ -28,6 +28,7 @@ export async function runPairCommand(options: PairOptions): Promise<void> {
relayEnabled: config.relayEnabled,
relayEndpoint: config.relayEndpoint,
relayPublicEndpoint: config.relayPublicEndpoint,
relayUseTls: config.relayUseTls,
appBaseUrl: config.appBaseUrl,
includeQr: true,
});
@@ -21,6 +21,7 @@ export function startCommand(): Command {
.option("--home <path>", "Paseo home directory (default: ~/.paseo)")
.option("--foreground", "Run in foreground (don't daemonize)")
.option("--no-relay", "Disable relay connection")
.option("--relay-use-tls", "Use wss:// for the relay connection and pairing offers")
.option("--no-mcp", "Disable the Agent MCP HTTP endpoint")
.option("--no-inject-mcp", "Disable auto-injecting the Paseo MCP into created agents")
.option(
@@ -18,6 +18,7 @@ interface DaemonStatus {
connectedDaemon: "reachable" | "unreachable" | "not_probed";
home: string;
listen: string;
relay: string;
hostname: string | null;
pid: number | null;
startedAt: string | null;
@@ -117,6 +118,7 @@ function toStatusRows(status: DaemonStatus): StatusRow[] {
{ key: "Connected Daemon", value: status.connectedDaemon },
{ key: "Home", value: status.home },
{ key: "Listen", value: status.listen },
{ key: "Relay", value: status.relay },
{ key: "Hostname", value: status.hostname ?? "-" },
{ key: "PID", value: status.pid === null ? "-" : String(status.pid) },
{ key: "Started", value: status.startedAt ?? "-" },
@@ -312,6 +314,12 @@ async function resolveDaemonNodeLabel(
return fromPid.nodePath ?? `unknown (${fromPid.error ?? "could not resolve from PID"})`;
}
function formatRelayStatus(state: ReturnType<typeof resolveLocalDaemonState>): string {
if (!state.relayEnabled) return "disabled";
const scheme = state.relayUseTls ? "wss" : "ws";
return `${scheme}://${state.relayEndpoint}`;
}
export type StatusResult = ListResult<StatusRow>;
export async function runStatusCommand(
@@ -370,6 +378,7 @@ export async function runStatusCommand(
connectedDaemon,
home: state.home,
listen: state.listen,
relay: formatRelayStatus(state),
hostname: state.pidInfo?.hostname ?? null,
pid: state.pidInfo?.pid ?? null,
startedAt: state.pidInfo?.startedAt ?? null,
+1
View File
@@ -491,6 +491,7 @@ export async function runOnboard(options: OnboardOptions): Promise<void> {
relayEnabled: config.relayEnabled,
relayEndpoint: config.relayEndpoint,
relayPublicEndpoint: config.relayPublicEndpoint,
relayUseTls: config.relayUseTls,
appBaseUrl: config.appBaseUrl,
includeQr: true,
});
+1 -1
View File
@@ -286,7 +286,7 @@ async function connectViaRelayOffer(
endpoint: offer.relay.endpoint,
serverId: offer.serverId,
role: "client",
useTls: shouldUseTlsForDefaultHostedRelay(offer.relay.endpoint),
useTls: offer.relay.useTls ?? shouldUseTlsForDefaultHostedRelay(offer.relay.endpoint),
});
const client = new DaemonClient({