fix(hub): harden relationship lifecycle

Keep optional Hub authority from blocking daemon startup, revoke ambiguous enrollments durably, and close owned agents when their relationship no longer exists. Reject remote CLI connect targets before transmitting enrollment authority.
This commit is contained in:
Mohamed Boudra
2026-07-13 12:58:06 +02:00
parent 8f5685b4c4
commit 9d9797c9e2
9 changed files with 285 additions and 46 deletions
@@ -0,0 +1,30 @@
import { describe, expect, test } from "vitest";
import { assertLocalHubManagementTarget } from "./index.js";
describe("Hub CLI target authority", () => {
test.each(["relay.paseo.test:443", "tcp://10.0.0.5:6767?ssl=true"])(
"rejects non-local explicit target %s before connect",
(host) => {
expect(() => assertLocalHubManagementTarget(host, {})).toThrow(
"Hub relationship management requires a local daemon target",
);
},
);
test.each([
undefined,
"localhost:6767",
"127.0.0.1:6767",
"[::1]:6767",
"[0:0:0:0:0:0:0:1]:6767",
"unix:///tmp/paseo.sock",
])("accepts local target %s", (host) => {
expect(() => assertLocalHubManagementTarget(host, {})).not.toThrow();
});
test("rejects a non-local PASEO_HOST target", () => {
expect(() =>
assertLocalHubManagementTarget(undefined, { PASEO_HOST: "hub.test:6767" }),
).toThrow("Hub relationship management requires a local daemon target");
});
});
+22 -1
View File
@@ -1,7 +1,7 @@
import { Command } from "commander";
import { withOutput, type ListResult, type OutputSchema } from "../../output/index.js";
import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js";
import { connectToDaemon } from "../../utils/client.js";
import { connectToDaemon, resolveDaemonTarget } from "../../utils/client.js";
interface HubRow {
state: string;
@@ -66,6 +66,26 @@ async function withClient<T>(
}
}
export function assertLocalHubManagementTarget(
host: string | undefined,
env: NodeJS.ProcessEnv = process.env,
): void {
const explicitHost = host ?? env.PASEO_HOST;
if (!explicitHost) return;
const target = resolveDaemonTarget(explicitHost);
if (target.type === "ipc") return;
const hostname = new URL(target.url).hostname.replace(/^\[|\]$/g, "").toLowerCase();
if (
hostname === "localhost" ||
hostname === "::1" ||
hostname === "0:0:0:0:0:0:0:1" ||
/^127(?:\.\d{1,3}){3}$/.test(hostname)
) {
return;
}
throw new Error("Hub relationship management requires a local daemon target");
}
export function createHubCommand(): Command {
const hub = new Command("hub").description("Manage this daemon's Paseo Hub relationship");
addJsonAndDaemonHostOptions(
@@ -74,6 +94,7 @@ export function createHubCommand(): Command {
withOutput(async (...args) => {
const url = args[0] as string;
const options = args.at(-2) as { token: string; host?: string };
assertLocalHubManagementTarget(options.host);
return withClient(options.host, async (client) =>
result((await client.connectHub(url, options.token)).status),
);