fix: tighten gateway bind auth diagnostics

This commit is contained in:
Peter Steinberger
2026-01-08 07:42:50 +01:00
parent debfce5a77
commit 5565dcd447
8 changed files with 196 additions and 3 deletions

View File

@@ -5,7 +5,13 @@ import {
DEFAULT_GATEWAY_DAEMON_RUNTIME,
isGatewayDaemonRuntime,
} from "../commands/daemon-runtime.js";
import { loadConfig, resolveGatewayPort } from "../config/config.js";
import {
createConfigIO,
loadConfig,
resolveConfigPath,
resolveGatewayPort,
resolveStateDir,
} from "../config/config.js";
import { resolveIsNixMode } from "../config/paths.js";
import {
GATEWAY_LAUNCH_AGENT_LABEL,
@@ -17,11 +23,13 @@ import {
findExtraGatewayServices,
renderGatewayServiceCleanupHints,
} from "../daemon/inspect.js";
import { readLastGatewayErrorLine } from "../daemon/diagnostics.js";
import { resolveGatewayLogPaths } from "../daemon/launchd.js";
import { findLegacyGatewayServices } from "../daemon/legacy.js";
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
import { resolveGatewayService } from "../daemon/service.js";
import { callGateway } from "../gateway/call.js";
import { resolveGatewayBindHost } from "../gateway/net.js";
import {
formatPortDiagnostics,
inspectPortUsage,
@@ -33,6 +41,22 @@ import { defaultRuntime } from "../runtime.js";
import { createDefaultDeps } from "./deps.js";
import { withProgress } from "./progress.js";
type ConfigSummary = {
path: string;
exists: boolean;
valid: boolean;
issues?: Array<{ path: string; message: string }>;
};
type GatewayStatusSummary = {
bindMode: string;
bindHost: string | null;
port: number;
portSource: "service args" | "env/config";
probeUrl: string;
probeNote?: string;
};
type DaemonStatus = {
service: {
label: string;
@@ -42,6 +66,8 @@ type DaemonStatus = {
command?: {
programArguments: string[];
workingDirectory?: string;
environment?: Record<string, string>;
sourcePath?: string;
} | null;
runtime?: {
status?: string;
@@ -57,15 +83,29 @@ type DaemonStatus = {
missingUnit?: boolean;
};
};
config?: {
cli: ConfigSummary;
daemon?: ConfigSummary;
mismatch?: boolean;
};
gateway?: GatewayStatusSummary;
port?: {
port: number;
status: PortUsageStatus;
listeners: PortListener[];
hints: string[];
};
portCli?: {
port: number;
status: PortUsageStatus;
listeners: PortListener[];
hints: string[];
};
lastError?: string;
rpc?: {
ok: boolean;
error?: string;
url?: string;
};
legacyServices: Array<{ label: string; detail: string }>;
extraServices: Array<{ label: string; detail: string; scope: string }>;
@@ -253,6 +293,15 @@ async function gatherDaemonStatus(opts: {
deep: opts.deep,
});
const rpc = opts.probe ? await probeGatewayStatus(opts.rpc) : undefined;
let lastError: string | undefined;
if (
loaded &&
runtime?.status === "running" &&
portStatus &&
portStatus.status !== "busy"
) {
lastError = (await readLastGatewayErrorLine(process.env)) ?? undefined;
}
return {
service: {
@@ -264,6 +313,7 @@ async function gatherDaemonStatus(opts: {
runtime,
},
port: portStatus,
lastError,
rpc,
legacyServices,
extraServices,
@@ -334,6 +384,28 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
defaultRuntime.error(line);
}
}
if (
service.loaded &&
service.runtime?.status === "running" &&
status.port &&
status.port.status !== "busy"
) {
defaultRuntime.error(
`Gateway port ${status.port.port} is not listening (service appears running).`,
);
if (status.lastError) {
defaultRuntime.error(`Last gateway error: ${status.lastError}`);
}
if (process.platform === "linux") {
defaultRuntime.error(
`Logs: journalctl --user -u ${GATEWAY_SYSTEMD_SERVICE_NAME}.service -n 200 --no-pager`,
);
} else if (process.platform === "darwin") {
const logs = resolveGatewayLogPaths(process.env);
defaultRuntime.error(`Logs: ${logs.stdoutPath}`);
defaultRuntime.error(`Errors: ${logs.stderrPath}`);
}
}
if (legacyServices.length > 0) {
defaultRuntime.error("Legacy Clawdis services detected:");

View File

@@ -4,6 +4,7 @@ import type { Command } from "commander";
import {
CONFIG_PATH_CLAWDBOT,
loadConfig,
readConfigFileSnapshot,
resolveGatewayPort,
} from "../config/config.js";
import {
@@ -70,6 +71,26 @@ function describeUnknownError(err: unknown): string {
return "Unknown error";
}
function extractGatewayMiskeys(parsed: unknown): {
hasGatewayToken: boolean;
hasRemoteToken: boolean;
} {
if (!parsed || typeof parsed !== "object") {
return { hasGatewayToken: false, hasRemoteToken: false };
}
const gateway = (parsed as Record<string, unknown>).gateway;
if (!gateway || typeof gateway !== "object") {
return { hasGatewayToken: false, hasRemoteToken: false };
}
const hasGatewayToken = "token" in (gateway as Record<string, unknown>);
const remote = (gateway as Record<string, unknown>).remote;
const hasRemoteToken =
remote && typeof remote === "object"
? "token" in (remote as Record<string, unknown>)
: false;
return { hasGatewayToken, hasRemoteToken };
}
function renderGatewayServiceStopHints(): string[] {
switch (process.platform) {
case "darwin":
@@ -368,7 +389,7 @@ export function registerGatewayCli(program: Command) {
);
} else {
defaultRuntime.error(
"Gateway start blocked: set gateway.mode=local (or pass --allow-unconfigured).",
`Gateway start blocked: set gateway.mode=local (current: ${mode ?? "unset"}) or pass --allow-unconfigured.`,
);
}
defaultRuntime.exit(1);
@@ -390,6 +411,72 @@ export function registerGatewayCli(program: Command) {
return;
}
const snapshot = await readConfigFileSnapshot().catch(() => null);
const miskeys = extractGatewayMiskeys(snapshot?.parsed);
const authModeFromConfig = cfg.gateway?.auth?.mode;
const tokenValue =
opts.token ??
cfg.gateway?.auth?.token ??
process.env.CLAWDBOT_GATEWAY_TOKEN;
const passwordValue =
opts.password ??
cfg.gateway?.auth?.password ??
process.env.CLAWDBOT_GATEWAY_PASSWORD;
const resolvedAuthMode =
authMode ??
authModeFromConfig ??
(passwordValue ? "password" : tokenValue ? "token" : "none");
const authHints: string[] = [];
if (miskeys.hasGatewayToken) {
authHints.push(
'Found "gateway.token" in config. Use "gateway.auth.token" instead.',
);
}
if (miskeys.hasRemoteToken) {
authHints.push(
'"gateway.remote.token" is for remote CLI calls; it does not enable local gateway auth.',
);
}
if (resolvedAuthMode === "token" && !tokenValue) {
defaultRuntime.error(
[
"Gateway auth is set to token, but no token is configured.",
"Set gateway.auth.token (or CLAWDBOT_GATEWAY_TOKEN), or pass --token.",
...authHints,
]
.filter(Boolean)
.join("\n"),
);
defaultRuntime.exit(1);
return;
}
if (resolvedAuthMode === "password" && !passwordValue) {
defaultRuntime.error(
[
"Gateway auth is set to password, but no password is configured.",
"Set gateway.auth.password (or CLAWDBOT_GATEWAY_PASSWORD), or pass --password.",
...authHints,
]
.filter(Boolean)
.join("\n"),
);
defaultRuntime.exit(1);
return;
}
if (bind !== "loopback" && resolvedAuthMode === "none") {
defaultRuntime.error(
[
`Refusing to bind gateway to ${bind} without auth.`,
"Set gateway.auth.token (or CLAWDBOT_GATEWAY_TOKEN) or pass --token.",
...authHints,
]
.filter(Boolean)
.join("\n"),
);
defaultRuntime.exit(1);
return;
}
try {
await runGatewayLoop({
runtime: defaultRuntime,

View File

@@ -59,6 +59,20 @@ export type ConfigIoDeps = {
logger?: Pick<typeof console, "error" | "warn">;
};
function warnOnConfigMiskeys(
raw: unknown,
logger: Pick<typeof console, "warn">,
): void {
if (!raw || typeof raw !== "object") return;
const gateway = (raw as Record<string, unknown>).gateway;
if (!gateway || typeof gateway !== "object") return;
if ("token" in (gateway as Record<string, unknown>)) {
logger.warn(
'Config uses "gateway.token". This key is ignored; use "gateway.auth.token" instead.',
);
}
}
function resolveConfigPathForDeps(deps: Required<ConfigIoDeps>): string {
if (deps.configPath) return deps.configPath;
return resolveConfigPath(deps.env, resolveStateDir(deps.env, deps.homedir));
@@ -106,6 +120,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
}
const raw = deps.fs.readFileSync(configPath, "utf-8");
const parsed = deps.json5.parse(raw);
warnOnConfigMiskeys(parsed, deps.logger);
if (typeof parsed !== "object" || parsed === null) return {};
const validated = ClawdbotSchema.safeParse(parsed);
if (!validated.success) {

View File

@@ -464,7 +464,7 @@ export async function startGatewayServer(
}
if (!isLoopbackHost(bindHost) && authMode === "none") {
throw new Error(
`refusing to bind gateway to ${bindHost}:${port} without auth (set gateway.auth or CLAWDBOT_GATEWAY_TOKEN)`,
`refusing to bind gateway to ${bindHost}:${port} without auth (set gateway.auth.token or CLAWDBOT_GATEWAY_TOKEN, or pass --token)`,
);
}