From 087cc71c007444df3f294525af261a1888028c3d Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 2 Oct 2026 18:27:51 +0100 Subject: [PATCH] fix(server): use macOS ComputerName for host identity (#5922) * fix(server): advertise macOS ComputerName macOS may expose UNKNOWN through its kernel hostname when DHCP or reverse DNS does not provide one. Use the user-configured ComputerName for daemon host identity instead. * test(server): cover macOS Hub host name --- .../src/server/bootstrap-web-ui.test.ts | 30 +++++++++++++++++-- packages/server/src/server/bootstrap.ts | 8 ++--- packages/server/src/server/host-name.ts | 14 +++++++++ .../hub/relationship-controller.test.ts | 5 ++-- packages/server/src/server/pid-lock.ts | 5 ++-- .../server/src/server/websocket-server.ts | 4 +-- 6 files changed, 53 insertions(+), 13 deletions(-) create mode 100644 packages/server/src/server/host-name.ts diff --git a/packages/server/src/server/bootstrap-web-ui.test.ts b/packages/server/src/server/bootstrap-web-ui.test.ts index 348a6cf0eb..3822cfaa55 100644 --- a/packages/server/src/server/bootstrap-web-ui.test.ts +++ b/packages/server/src/server/bootstrap-web-ui.test.ts @@ -2,10 +2,17 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import http from "node:http"; import os from "node:os"; import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { DaemonClient } from "@getpaseo/client/internal/daemon-client"; import { afterEach, describe, expect, test } from "vitest"; import { createTestPaseoDaemon, type TestPaseoDaemon } from "./test-utils/paseo-daemon.js"; +const expectedHostName = + process.platform === "darwin" + ? execFileSync("/usr/sbin/scutil", ["--get", "ComputerName"], { encoding: "utf8" }).trim() + : os.hostname(); + interface InitialDaemonConnectionHint { listen: string; useTls: boolean; @@ -77,6 +84,23 @@ describe("daemon web UI bootstrap", () => { } }); + test("advertises ComputerName as the hostname over HTTP and WebSocket", async () => { + daemonHandle = await createTestPaseoDaemon({ mcpEnabled: false }); + const response = await fetch(`http://127.0.0.1:${daemonHandle.port}/api/status`); + expect(await response.json()).toMatchObject({ hostname: expectedHostName }); + const client = new DaemonClient({ + url: `ws://127.0.0.1:${daemonHandle.port}/ws`, + clientId: "host-name-test", + clientType: "mobile", + }); + try { + await client.connect(); + expect(client.getLastServerInfoMessage()?.hostname).toBe(expectedHostName); + } finally { + await client.close(); + } + }); + test("injects a TLS initial connection hint only for HTTPS forwarded by a trusted proxy", async () => { const distDir = await createWebUiDist(); @@ -101,12 +125,12 @@ describe("daemon web UI bootstrap", () => { expect(httpHint).toEqual({ listen: `daemon.example.test:${daemonHandle.port}`, useTls: false, - label: os.hostname(), + label: expectedHostName, }); expect(httpsHint).toEqual({ listen: `daemon.example.test:${daemonHandle.port}`, useTls: true, - label: os.hostname(), + label: expectedHostName, }); }); @@ -132,7 +156,7 @@ describe("daemon web UI bootstrap", () => { expect(httpsHint).toEqual({ listen: `daemon.example.test:${daemonHandle.port}`, useTls: false, - label: os.hostname(), + label: expectedHostName, }); }); }); diff --git a/packages/server/src/server/bootstrap.ts b/packages/server/src/server/bootstrap.ts index cf7b147929..659ac54d02 100644 --- a/packages/server/src/server/bootstrap.ts +++ b/packages/server/src/server/bootstrap.ts @@ -4,7 +4,7 @@ import { createServer as createHTTPServer, type IncomingMessage, type ServerResp import { constants, existsSync, unlinkSync } from "fs"; import { open, rm, stat } from "fs/promises"; import { randomUUID } from "node:crypto"; -import { hostname as getHostname } from "node:os"; +import { getHostName } from "./host-name.js"; import path from "node:path"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import type { Logger } from "pino"; @@ -523,7 +523,7 @@ function mountWebUi(app: express.Application, config: PaseoDaemonConfig, logger: createWebUiMiddleware({ enabled: config.webUi?.enabled ?? false, distDir: config.webUi?.distDir ?? null, - label: getHostname(), + label: getHostName(), logger, }), ); @@ -794,7 +794,7 @@ export async function createPaseoDaemon( res.json({ status: "server_info", serverId, - hostname: getHostname(), + hostname: getHostName(), version: daemonVersion, listen: formatListenTarget(boundListenTarget ?? listenTarget), }); @@ -1235,7 +1235,7 @@ export async function createPaseoDaemon( }); const hubRelationships = new HubRelationshipController({ paseoHome: config.paseoHome, - hostname: getHostname(), + hostname: getHostName(), serverId, daemonPublicKey: daemonKeyPair.publicKeyB64, logger, diff --git a/packages/server/src/server/host-name.ts b/packages/server/src/server/host-name.ts new file mode 100644 index 0000000000..5f36dcc368 --- /dev/null +++ b/packages/server/src/server/host-name.ts @@ -0,0 +1,14 @@ +import { execFileSync } from "node:child_process"; +import { hostname } from "node:os"; + +let hostName: string | undefined; + +// macOS's kernel hostname comes from DHCP/reverse DNS and can be "UNKNOWN"; +// ComputerName is the name the user gave the Mac. +export function getHostName(): string { + hostName ??= + process.platform === "darwin" + ? execFileSync("/usr/sbin/scutil", ["--get", "ComputerName"], { encoding: "utf8" }).trim() + : hostname(); + return hostName; +} diff --git a/packages/server/src/server/hub/relationship-controller.test.ts b/packages/server/src/server/hub/relationship-controller.test.ts index f632971cc9..51596b2384 100644 --- a/packages/server/src/server/hub/relationship-controller.test.ts +++ b/packages/server/src/server/hub/relationship-controller.test.ts @@ -1,6 +1,7 @@ import { createHash } from "node:crypto"; -import { hostname, platform } from "node:os"; +import { platform } from "node:os"; import { afterEach, describe, expect, test } from "vitest"; +import { getHostName } from "../host-name.js"; import { HubRelationshipHarness } from "./test-utils/relationship-harness.js"; async function captureUnhandledRejections(action: () => Promise): Promise { @@ -88,7 +89,7 @@ describe("Hub relationship", () => { .update(secret ?? "") .digest("base64url"), ); - expect(enrollment.hostname).toBe(hostname()); + expect(enrollment.hostname).toBe(getHostName()); relationship.completeEnrollment(); await connecting.result; await relationship.socketDialed(); diff --git a/packages/server/src/server/pid-lock.ts b/packages/server/src/server/pid-lock.ts index 551d5be392..db95d4786d 100644 --- a/packages/server/src/server/pid-lock.ts +++ b/packages/server/src/server/pid-lock.ts @@ -2,7 +2,8 @@ import { open, readFile, stat, unlink, utimes } from "node:fs/promises"; import type { FileHandle } from "node:fs/promises"; import { ensurePrivateDirectory } from "./private-files.js"; import { join } from "node:path"; -import { hostname, uptime } from "node:os"; +import { uptime } from "node:os"; +import { getHostName } from "./host-name.js"; import { z } from "zod"; export const pidLockInfoSchema = z.object({ @@ -216,7 +217,7 @@ export async function acquirePidLock( const lockInfo: PidLockInfo = { pid: lockOwnerPid, startedAt: new Date().toISOString(), - hostname: hostname(), + hostname: getHostName(), uid: process.getuid?.() ?? 0, listen, heartbeat: true, diff --git a/packages/server/src/server/websocket-server.ts b/packages/server/src/server/websocket-server.ts index 30a9e59aa9..67c78184fb 100644 --- a/packages/server/src/server/websocket-server.ts +++ b/packages/server/src/server/websocket-server.ts @@ -5,7 +5,7 @@ import { MessageReceipts } from "./message-receipts/index.js"; import { WebSocket, WebSocketServer } from "ws"; import type { IncomingMessage, Server as HTTPServer } from "http"; import { join } from "path"; -import { hostname as getHostname } from "node:os"; +import { getHostName } from "./host-name.js"; import { randomUUID } from "node:crypto"; import { monitorEventLoopDelay } from "node:perf_hooks"; import type { AgentManager, AgentMetricsSnapshot } from "./agent/agent-manager.js"; @@ -1772,7 +1772,7 @@ export class VoiceAssistantWebSocketServer { status: "server_info", protocolVersion: WS_PROTOCOL_VERSION, serverId: this.serverId, - hostname: getHostname(), + hostname: getHostName(), version: this.daemonVersion, permissions: session.getPermissions(), // COMPAT(desktopManaged): added in v0.1.X, remove optional parsing after 2027-01-16.