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
This commit is contained in:
Mohamed Boudra
2026-10-02 18:27:51 +01:00
committed by GitHub
parent 3b5bbb139b
commit 087cc71c00
6 changed files with 53 additions and 13 deletions
@@ -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,
});
});
});
+4 -4
View File
@@ -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,
+14
View File
@@ -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;
}
@@ -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<void>): Promise<unknown[]> {
@@ -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();
+3 -2
View File
@@ -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,
@@ -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.