Report daemon auth failures in status
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import { createRequire } from "node:module";
|
import { createRequire } from "node:module";
|
||||||
import { getOrCreateServerId, findExecutable, execCommand } from "@getpaseo/server";
|
import { getOrCreateServerId, findExecutable, execCommand } from "@getpaseo/server";
|
||||||
import { tryConnectToDaemon } from "../../utils/client.js";
|
import { connectToDaemon } from "../../utils/client.js";
|
||||||
import type { CommandOptions, ListResult, OutputSchema } from "../../output/index.js";
|
import type { CommandOptions, ListResult, OutputSchema } from "../../output/index.js";
|
||||||
import { resolveLocalDaemonState, resolveTcpHostFromListen } from "./local-daemon.js";
|
import { resolveLocalDaemonState, resolveTcpHostFromListen } from "./local-daemon.js";
|
||||||
import { resolveNodePathFromPid } from "./runtime-toolchain.js";
|
import { resolveNodePathFromPid } from "./runtime-toolchain.js";
|
||||||
@@ -16,7 +16,7 @@ interface ProviderBinaryStatus {
|
|||||||
interface DaemonStatus {
|
interface DaemonStatus {
|
||||||
serverId: string | null;
|
serverId: string | null;
|
||||||
localDaemon: "running" | "stopped" | "stale_pid" | "unresponsive";
|
localDaemon: "running" | "stopped" | "stale_pid" | "unresponsive";
|
||||||
connectedDaemon: "reachable" | "unreachable" | "not_probed";
|
connectedDaemon: "reachable" | "unreachable" | "auth_required" | "auth_failed" | "not_probed";
|
||||||
home: string;
|
home: string;
|
||||||
listen: string;
|
listen: string;
|
||||||
relay: string;
|
relay: string;
|
||||||
@@ -33,6 +33,7 @@ interface DaemonStatus {
|
|||||||
daemonVersion: string | null;
|
daemonVersion: string | null;
|
||||||
desktopManaged: boolean;
|
desktopManaged: boolean;
|
||||||
providers: ProviderBinaryStatus[];
|
providers: ProviderBinaryStatus[];
|
||||||
|
agentsUnavailableReason?: string;
|
||||||
note?: string;
|
note?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +97,7 @@ function createStatusSchema(status: DaemonStatus): OutputSchema<StatusRow> {
|
|||||||
}
|
}
|
||||||
if (item.key === "Connected Daemon") {
|
if (item.key === "Connected Daemon") {
|
||||||
if (item.value === "reachable") return "green";
|
if (item.value === "reachable") return "green";
|
||||||
if (item.value === "not_probed") return "yellow";
|
if (item.value === "not_probed" || item.value === "auth_required") return "yellow";
|
||||||
return "red";
|
return "red";
|
||||||
}
|
}
|
||||||
if (item.key.startsWith(" ")) {
|
if (item.key.startsWith(" ")) {
|
||||||
@@ -139,7 +140,7 @@ function toStatusRows(status: DaemonStatus): StatusRow[] {
|
|||||||
} else {
|
} else {
|
||||||
rows.push({
|
rows.push({
|
||||||
key: "Agents",
|
key: "Agents",
|
||||||
value: "Unavailable (daemon API not reachable)",
|
value: `Unavailable (${status.agentsUnavailableReason ?? "daemon API not reachable"})`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,16 +219,49 @@ interface DaemonProbeResult {
|
|||||||
idleAgents?: number;
|
idleAgents?: number;
|
||||||
daemonNodeOverride?: string;
|
daemonNodeOverride?: string;
|
||||||
daemonProviders?: ProviderBinaryStatus[];
|
daemonProviders?: ProviderBinaryStatus[];
|
||||||
|
agentsUnavailableReason?: string;
|
||||||
note?: string;
|
note?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DaemonAuthProbeFailure = "auth_required" | "auth_failed";
|
||||||
|
|
||||||
|
function classifyDaemonAuthProbeFailure(error: unknown): DaemonAuthProbeFailure | null {
|
||||||
|
if (!(error instanceof Error)) return null;
|
||||||
|
if (error.message === "Password required") return "auth_required";
|
||||||
|
if (error.message === "Incorrect password") return "auth_failed";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeDaemonAuthProbeFailure(host: string, failure: DaemonAuthProbeFailure): string {
|
||||||
|
if (failure === "auth_required") {
|
||||||
|
return `Daemon is reachable at ${host} but requires a password. Set PASEO_PASSWORD and retry.`;
|
||||||
|
}
|
||||||
|
return `Daemon is reachable at ${host} but the supplied password was rejected. Check PASEO_PASSWORD and retry.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeAgentsUnavailableReason(failure: DaemonAuthProbeFailure): string {
|
||||||
|
if (failure === "auth_required") return "password required";
|
||||||
|
return "incorrect password";
|
||||||
|
}
|
||||||
|
|
||||||
async function probeDaemonOverWebsocket(args: {
|
async function probeDaemonOverWebsocket(args: {
|
||||||
host: string;
|
host: string;
|
||||||
state: ReturnType<typeof resolveLocalDaemonState>;
|
state: ReturnType<typeof resolveLocalDaemonState>;
|
||||||
}): Promise<DaemonProbeResult> {
|
}): Promise<DaemonProbeResult> {
|
||||||
const { host, state } = args;
|
const { host, state } = args;
|
||||||
const client = await tryConnectToDaemon({ host, timeout: 1500 });
|
let client: Awaited<ReturnType<typeof connectToDaemon>>;
|
||||||
if (!client) {
|
try {
|
||||||
|
client = await connectToDaemon({ host, timeout: 1500 });
|
||||||
|
} catch (error) {
|
||||||
|
const authFailure = classifyDaemonAuthProbeFailure(error);
|
||||||
|
if (authFailure) {
|
||||||
|
return {
|
||||||
|
connectedDaemon: authFailure,
|
||||||
|
agentsUnavailableReason: describeAgentsUnavailableReason(authFailure),
|
||||||
|
note: describeDaemonAuthProbeFailure(host, authFailure),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (state.running) {
|
if (state.running) {
|
||||||
return {
|
return {
|
||||||
connectedDaemon: "unreachable",
|
connectedDaemon: "unreachable",
|
||||||
@@ -309,6 +343,7 @@ interface ProbeMergeState {
|
|||||||
runningAgents: number | null;
|
runningAgents: number | null;
|
||||||
idleAgents: number | null;
|
idleAgents: number | null;
|
||||||
daemonProviders: ProviderBinaryStatus[] | undefined;
|
daemonProviders: ProviderBinaryStatus[] | undefined;
|
||||||
|
agentsUnavailableReason: string | undefined;
|
||||||
note: string | undefined;
|
note: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,6 +357,7 @@ function applyProbeToStatus(input: ProbeMergeState): Omit<ProbeMergeState, "prob
|
|||||||
runningAgents: probe.runningAgents !== undefined ? probe.runningAgents : input.runningAgents,
|
runningAgents: probe.runningAgents !== undefined ? probe.runningAgents : input.runningAgents,
|
||||||
idleAgents: probe.idleAgents !== undefined ? probe.idleAgents : input.idleAgents,
|
idleAgents: probe.idleAgents !== undefined ? probe.idleAgents : input.idleAgents,
|
||||||
daemonProviders: probe.daemonProviders ?? input.daemonProviders,
|
daemonProviders: probe.daemonProviders ?? input.daemonProviders,
|
||||||
|
agentsUnavailableReason: probe.agentsUnavailableReason ?? input.agentsUnavailableReason,
|
||||||
note: probe.note ? appendNote(input.note, probe.note) : input.note,
|
note: probe.note ? appendNote(input.note, probe.note) : input.note,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -371,6 +407,7 @@ export async function runStatusCommand(
|
|||||||
let idleAgents: number | null = null;
|
let idleAgents: number | null = null;
|
||||||
let daemonVersion: string | null = null;
|
let daemonVersion: string | null = null;
|
||||||
let daemonProviders: ProviderBinaryStatus[] | undefined;
|
let daemonProviders: ProviderBinaryStatus[] | undefined;
|
||||||
|
let agentsUnavailableReason: string | undefined;
|
||||||
let note: string | undefined;
|
let note: string | undefined;
|
||||||
|
|
||||||
if (!state.running && state.stalePidFile && state.pidInfo) {
|
if (!state.running && state.stalePidFile && state.pidInfo) {
|
||||||
@@ -388,6 +425,7 @@ export async function runStatusCommand(
|
|||||||
runningAgents,
|
runningAgents,
|
||||||
idleAgents,
|
idleAgents,
|
||||||
daemonProviders,
|
daemonProviders,
|
||||||
|
agentsUnavailableReason,
|
||||||
note,
|
note,
|
||||||
} = applyProbeToStatus({
|
} = applyProbeToStatus({
|
||||||
probe,
|
probe,
|
||||||
@@ -398,6 +436,7 @@ export async function runStatusCommand(
|
|||||||
runningAgents,
|
runningAgents,
|
||||||
idleAgents,
|
idleAgents,
|
||||||
daemonProviders,
|
daemonProviders,
|
||||||
|
agentsUnavailableReason,
|
||||||
note,
|
note,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
@@ -434,6 +473,7 @@ export async function runStatusCommand(
|
|||||||
daemonVersion,
|
daemonVersion,
|
||||||
desktopManaged: state.pidInfo?.desktopManaged === true,
|
desktopManaged: state.pidInfo?.desktopManaged === true,
|
||||||
providers,
|
providers,
|
||||||
|
agentsUnavailableReason,
|
||||||
note,
|
note,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env npx tsx
|
||||||
|
|
||||||
|
import assert from "node:assert";
|
||||||
|
import { writeFile } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { createTestPaseoDaemon } from "../../server/src/server/test-utils/paseo-daemon.ts";
|
||||||
|
import { runLocalPaseo } from "./helpers/local-cli.ts";
|
||||||
|
|
||||||
|
console.log("=== Daemon Status Auth ===\n");
|
||||||
|
|
||||||
|
const CORRECT_PASSWORD_HASH = "$2b$12$GMhF7pN4QnMlHOQXOqjd1OitKWPSmAO3FwB0PHzKtcZR/sAMryz76";
|
||||||
|
|
||||||
|
const daemon = await createTestPaseoDaemon({
|
||||||
|
auth: { password: CORRECT_PASSWORD_HASH },
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await writeFile(
|
||||||
|
join(daemon.paseoHome, "paseo.pid"),
|
||||||
|
`${JSON.stringify(
|
||||||
|
{
|
||||||
|
pid: process.pid,
|
||||||
|
startedAt: new Date().toISOString(),
|
||||||
|
hostname: "status-auth-test",
|
||||||
|
uid: process.getuid?.(),
|
||||||
|
listen: `0.0.0.0:${daemon.port}`,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
console.log("Test 1: status reports password requirement without marking daemon unreachable");
|
||||||
|
const result = await runLocalPaseo(["daemon", "status", "--json"], {
|
||||||
|
PASEO_HOME: daemon.paseoHome,
|
||||||
|
PASEO_HOST: "",
|
||||||
|
PASEO_PASSWORD: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(result.exitCode, 0, "status should still succeed");
|
||||||
|
const status = JSON.parse(result.stdout);
|
||||||
|
|
||||||
|
assert.strictEqual(status.localDaemon, "running");
|
||||||
|
assert.strictEqual(status.connectedDaemon, "auth_required");
|
||||||
|
assert.strictEqual(status.runningAgents, null);
|
||||||
|
assert.strictEqual(status.idleAgents, null);
|
||||||
|
assert.match(status.note, /requires a password/i);
|
||||||
|
assert.doesNotMatch(status.note, /not reachable/i);
|
||||||
|
console.log("✓ missing password reports auth_required\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
console.log("Test 2: status reports rejected supplied password separately");
|
||||||
|
const result = await runLocalPaseo(["daemon", "status", "--json"], {
|
||||||
|
PASEO_HOME: daemon.paseoHome,
|
||||||
|
PASEO_HOST: "",
|
||||||
|
PASEO_PASSWORD: "wrong-secret",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(result.exitCode, 0, "status should still succeed");
|
||||||
|
const status = JSON.parse(result.stdout);
|
||||||
|
|
||||||
|
assert.strictEqual(status.localDaemon, "running");
|
||||||
|
assert.strictEqual(status.connectedDaemon, "auth_failed");
|
||||||
|
assert.match(status.note, /password was rejected/i);
|
||||||
|
assert.doesNotMatch(status.note, /not reachable/i);
|
||||||
|
console.log("✓ wrong password reports auth_failed\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
console.log("Test 3: status reaches the same daemon when password is supplied");
|
||||||
|
const result = await runLocalPaseo(["daemon", "status", "--json"], {
|
||||||
|
PASEO_HOME: daemon.paseoHome,
|
||||||
|
PASEO_HOST: "",
|
||||||
|
PASEO_PASSWORD: "shared-secret",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(result.exitCode, 0, "status should succeed with password");
|
||||||
|
const status = JSON.parse(result.stdout);
|
||||||
|
|
||||||
|
assert.strictEqual(status.localDaemon, "running");
|
||||||
|
assert.strictEqual(status.connectedDaemon, "reachable");
|
||||||
|
assert.strictEqual(status.runningAgents, 0);
|
||||||
|
assert.strictEqual(status.idleAgents, 0);
|
||||||
|
console.log("✓ password-authenticated status remains reachable\n");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await daemon.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("=== Daemon Status Auth Tests Passed ===");
|
||||||
Reference in New Issue
Block a user