mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
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:
@@ -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,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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user