Use daemon hostnames for initial Hub slugs (#3172)

* fix(hub): send hostname during daemon enrollment

Pass the local hostname as daemon enrollment metadata so Hub can assign useful default slugs without coupling daemon authority to the human CLI login. Document the rollout dependency on Hub accepting the optional field.

* test(hub): verify enrollment hostname payload
This commit is contained in:
Mohamed Boudra
2026-08-10 21:55:55 +02:00
committed by GitHub
parent 94bda1f925
commit df9e8e8f06
6 changed files with 27 additions and 5 deletions
+1
View File
@@ -1111,6 +1111,7 @@ export async function createPaseoDaemon(
});
const hubRelationships = new HubRelationshipController({
paseoHome: config.paseoHome,
hostname: getHostname(),
serverId,
daemonPublicKey: daemonKeyPair.publicKeyB64,
logger,
@@ -1,5 +1,5 @@
import { createHash } from "node:crypto";
import { platform } from "node:os";
import { hostname, platform } from "node:os";
import { afterEach, describe, expect, test } from "vitest";
import { HubRelationshipHarness } from "./test-utils/relationship-harness.js";
@@ -88,6 +88,7 @@ describe("Hub relationship", () => {
.update(secret ?? "")
.digest("base64url"),
);
expect(enrollment.hostname).toBe(hostname());
relationship.completeEnrollment();
await connecting.result;
await relationship.socketDialed();
@@ -137,6 +137,7 @@ export interface HubRelationshipRetryPolicy {
export interface HubRelationshipControllerOptions {
paseoHome: string;
hostname: string;
serverId: string;
daemonPublicKey: string;
logger: pino.Logger;
@@ -324,6 +325,7 @@ export class HubRelationshipController implements HubRelationshipManagement {
idempotencyKey: pending.relationship.idempotencyKey,
hubOrigin: pending.relationship.hubOrigin,
token: pending.enrollment.token,
hostname: this.options.hostname,
serverId: pending.identity.serverId,
daemonPublicKey: pending.identity.daemonPublicKey,
credentialVerifier: verifier,
@@ -61,6 +61,7 @@ test.each([408, 429])("transient enrollment status %s remains retryable", async
idempotencyKey: "ceremony-1",
hubOrigin,
token: "token",
hostname: "test-daemon.local",
serverId: "server-1",
daemonPublicKey: "public-key",
credentialVerifier: "verifier",
@@ -74,7 +75,10 @@ test.each([408, 429])("transient enrollment status %s remains retryable", async
});
test("enrollment rejects a transport URL that cannot open a WebSocket", async () => {
const hubOrigin = await startEnrollmentHub("ftp://hub.test/daemon");
let enrolledHostname: string | undefined;
const hubOrigin = await startEnrollmentHub("ftp://hub.test/daemon", (enrollment) => {
enrolledHostname = enrollment.hostname;
});
const remote = new DirectHubRelationshipRemote();
await expect(
@@ -83,12 +87,14 @@ test("enrollment rejects a transport URL that cannot open a WebSocket", async ()
idempotencyKey: "ceremony-1",
hubOrigin,
token: "token",
hostname: "test-daemon.local",
serverId: "server-1",
daemonPublicKey: "public-key",
credentialVerifier: "verifier",
scopes: ["hub.execution.*"],
}),
).rejects.toThrow("Hub WebSocket URL must use ws or wss");
expect(enrolledHostname).toBe("test-daemon.local");
});
test("enrollment rejects a WebSocket URL with a fragment", async () => {
@@ -101,6 +107,7 @@ test("enrollment rejects a WebSocket URL with a fragment", async () => {
idempotencyKey: "ceremony-1",
hubOrigin,
token: "token",
hostname: "test-daemon.local",
serverId: "server-1",
daemonPublicKey: "public-key",
credentialVerifier: "verifier",
@@ -119,6 +126,7 @@ test("enrollment rejects a WebSocket outside the enrolled Hub authority", async
idempotencyKey: "ceremony-1",
hubOrigin,
token: "token",
hostname: "test-daemon.local",
serverId: "server-1",
daemonPublicKey: "public-key",
credentialVerifier: "verifier",
@@ -138,6 +146,7 @@ test.each(["enrollment", "revocation"])("%s HTTP calls are bounded", async (oper
idempotencyKey: "ceremony-1",
hubOrigin,
token: "token",
hostname: "test-daemon.local",
serverId: "server-1",
daemonPublicKey: "public-key",
credentialVerifier: "verifier",
@@ -290,11 +299,15 @@ async function startHubReturning(status: number): Promise<string> {
return `http://127.0.0.1:${address.port}`;
}
async function startEnrollmentHub(webSocketUrl: string): Promise<string> {
async function startEnrollmentHub(
webSocketUrl: string,
onEnrollment?: (enrollment: { daemonId: string; hostname: string }) => void,
): Promise<string> {
const server = createServer(async (request, response) => {
let body = "";
for await (const chunk of request) body += chunk;
const enrollment = JSON.parse(body) as { daemonId: string };
const enrollment = JSON.parse(body) as { daemonId: string; hostname: string };
onEnrollment?.(enrollment);
response.writeHead(200, { "content-type": "application/json" }).end(
JSON.stringify({
daemonId: enrollment.daemonId,
@@ -523,6 +536,7 @@ async function connectController(
openPaseoHomes.push(paseoHome);
const controller = new HubRelationshipController({
paseoHome,
hostname: "test-daemon.local",
serverId: "server-1",
daemonPublicKey: "daemon-public-key",
logger: pino({ level: "silent" }),
@@ -7,6 +7,7 @@ export interface HubEnrollment {
idempotencyKey: string;
hubOrigin: string;
token: string;
hostname: string;
serverId: string;
daemonPublicKey: string;
credentialVerifier: string;
@@ -96,6 +97,7 @@ export class DirectHubRelationshipRemote implements HubRelationshipRemote {
body: JSON.stringify({
daemonId: input.daemonId,
idempotencyKey: input.idempotencyKey,
hostname: input.hostname,
serverId: input.serverId,
daemonPublicKey: input.daemonPublicKey,
credentialVerifier: input.credentialVerifier,