fix: wire gateway auth diagnostics into doctor

This commit is contained in:
Peter Steinberger
2026-01-08 07:49:22 +01:00
parent 629eec11cc
commit b367ed75bf
8 changed files with 423 additions and 59 deletions

View File

@@ -36,6 +36,7 @@ import {
type PortListener,
type PortUsageStatus,
} from "../infra/ports.js";
import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
import { getResolvedLoggerSettings } from "../logging.js";
import { defaultRuntime } from "../runtime.js";
import { createDefaultDeps } from "./deps.js";
@@ -145,7 +146,56 @@ function parsePort(raw: unknown): number | null {
return parsed;
}
async function probeGatewayStatus(opts: GatewayRpcOpts) {
function parsePortFromArgs(
programArguments: string[] | undefined,
): number | null {
if (!programArguments?.length) return null;
for (let i = 0; i < programArguments.length; i += 1) {
const arg = programArguments[i];
if (arg === "--port") {
const next = programArguments[i + 1];
const parsed = parsePort(next);
if (parsed) return parsed;
}
if (arg?.startsWith("--port=")) {
const parsed = parsePort(arg.split("=", 2)[1]);
if (parsed) return parsed;
}
}
return null;
}
function pickProbeHostForBind(bindMode: string, tailnetIPv4: string | null) {
if (bindMode === "tailnet") return tailnetIPv4;
if (bindMode === "auto") return tailnetIPv4 ?? "127.0.0.1";
return "127.0.0.1";
}
function safeDaemonEnv(env: Record<string, string> | undefined): string[] {
if (!env) return [];
const allow = [
"CLAWDBOT_PROFILE",
"CLAWDBOT_STATE_DIR",
"CLAWDBOT_CONFIG_PATH",
"CLAWDBOT_GATEWAY_PORT",
"CLAWDBOT_NIX_MODE",
];
const lines: string[] = [];
for (const key of allow) {
const value = env[key];
if (!value?.trim()) continue;
lines.push(`${key}=${value.trim()}`);
}
return lines;
}
async function probeGatewayStatus(opts: {
url: string;
token?: string;
password?: string;
timeoutMs: number;
json?: boolean;
}) {
try {
await withProgress(
{
@@ -159,7 +209,7 @@ async function probeGatewayStatus(opts: GatewayRpcOpts) {
token: opts.token,
password: opts.password,
method: "status",
timeoutMs: Number(opts.timeout ?? 10_000),
timeoutMs: opts.timeoutMs,
clientName: "cli",
mode: "cli",
}),
@@ -209,6 +259,7 @@ function shouldReportPortUsage(
function renderRuntimeHints(
runtime: DaemonStatus["service"]["runtime"],
env: NodeJS.ProcessEnv = process.env,
): string[] {
if (!runtime) return [];
const hints: string[] = [];
@@ -227,7 +278,7 @@ function renderRuntimeHints(
if (runtime.status === "stopped") {
if (fileLog) hints.push(`File logs: ${fileLog}`);
if (process.platform === "darwin") {
const logs = resolveGatewayLogPaths(process.env);
const logs = resolveGatewayLogPaths(env);
hints.push(`Launchd stdout (if installed): ${logs.stdoutPath}`);
hints.push(`Launchd stderr (if installed): ${logs.stderrPath}`);
} else if (process.platform === "linux") {
@@ -272,27 +323,114 @@ async function gatherDaemonStatus(opts: {
service.readCommand(process.env).catch(() => null),
service.readRuntime(process.env).catch(() => undefined),
]);
let portStatus: DaemonStatus["port"] | undefined;
try {
const cfg = loadConfig();
if (cfg.gateway?.mode !== "remote") {
const port = resolveGatewayPort(cfg, process.env);
const diagnostics = await inspectPortUsage(port);
portStatus = {
port: diagnostics.port,
status: diagnostics.status,
listeners: diagnostics.listeners,
hints: diagnostics.hints,
};
}
} catch {
portStatus = undefined;
}
const serviceEnv = command?.environment ?? undefined;
const mergedDaemonEnv = {
...(process.env as Record<string, string | undefined>),
...(serviceEnv ?? {}),
} satisfies Record<string, string | undefined>;
const cliConfigPath = resolveConfigPath(process.env, resolveStateDir(process.env));
const daemonConfigPath = resolveConfigPath(
mergedDaemonEnv as NodeJS.ProcessEnv,
resolveStateDir(mergedDaemonEnv as NodeJS.ProcessEnv),
);
const cliIO = createConfigIO({ env: process.env, configPath: cliConfigPath });
const daemonIO = createConfigIO({
env: mergedDaemonEnv,
configPath: daemonConfigPath,
});
const [cliSnapshot, daemonSnapshot] = await Promise.all([
cliIO.readConfigFileSnapshot().catch(() => null),
daemonIO.readConfigFileSnapshot().catch(() => null),
]);
const cliCfg = cliIO.loadConfig();
const daemonCfg = daemonIO.loadConfig();
const cliConfigSummary: ConfigSummary = {
path: cliSnapshot?.path ?? cliConfigPath,
exists: cliSnapshot?.exists ?? false,
valid: cliSnapshot?.valid ?? true,
...(cliSnapshot?.issues?.length ? { issues: cliSnapshot.issues } : {}),
};
const daemonConfigSummary: ConfigSummary = {
path: daemonSnapshot?.path ?? daemonConfigPath,
exists: daemonSnapshot?.exists ?? false,
valid: daemonSnapshot?.valid ?? true,
...(daemonSnapshot?.issues?.length ? { issues: daemonSnapshot.issues } : {}),
};
const configMismatch = cliConfigSummary.path !== daemonConfigSummary.path;
const portFromArgs = parsePortFromArgs(command?.programArguments);
const daemonPort = portFromArgs ?? resolveGatewayPort(daemonCfg, mergedDaemonEnv);
const portSource: GatewayStatusSummary["portSource"] = portFromArgs
? "service args"
: "env/config";
const bindMode = daemonCfg.gateway?.bind ?? "loopback";
const bindHost = resolveGatewayBindHost(bindMode);
const tailnetIPv4 = pickPrimaryTailnetIPv4();
const probeHost = pickProbeHostForBind(bindMode, tailnetIPv4);
const probeUrlOverride =
typeof opts.rpc.url === "string" && opts.rpc.url.trim().length > 0
? opts.rpc.url.trim()
: null;
const probeUrl = probeUrlOverride ?? `ws://${probeHost}:${daemonPort}`;
const probeNote =
!probeUrlOverride && bindMode === "lan"
? "Local probe uses loopback (127.0.0.1); gateway bind=lan listens on 0.0.0.0."
: !probeUrlOverride && bindMode === "loopback"
? "Loopback-only gateway; only local clients can connect."
: undefined;
const cliPort = resolveGatewayPort(cliCfg, process.env);
const [portDiagnostics, portCliDiagnostics] = await Promise.all([
inspectPortUsage(daemonPort).catch(() => null),
cliPort !== daemonPort ? inspectPortUsage(cliPort).catch(() => null) : null,
]);
const portStatus: DaemonStatus["port"] | undefined = portDiagnostics
? {
port: portDiagnostics.port,
status: portDiagnostics.status,
listeners: portDiagnostics.listeners,
hints: portDiagnostics.hints,
}
: undefined;
const portCliStatus: DaemonStatus["portCli"] | undefined = portCliDiagnostics
? {
port: portCliDiagnostics.port,
status: portCliDiagnostics.status,
listeners: portCliDiagnostics.listeners,
hints: portCliDiagnostics.hints,
}
: undefined;
const legacyServices = await findLegacyGatewayServices(process.env);
const extraServices = await findExtraGatewayServices(process.env, {
deep: opts.deep,
});
const rpc = opts.probe ? await probeGatewayStatus(opts.rpc) : undefined;
const timeoutMsRaw = Number.parseInt(String(opts.rpc.timeout ?? "10000"), 10);
const timeoutMs =
Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0 ? timeoutMsRaw : 10_000;
const rpc = opts.probe
? await probeGatewayStatus({
url: probeUrl,
token:
opts.rpc.token ||
mergedDaemonEnv.CLAWDBOT_GATEWAY_TOKEN ||
daemonCfg.gateway?.auth?.token,
password:
opts.rpc.password ||
mergedDaemonEnv.CLAWDBOT_GATEWAY_PASSWORD ||
daemonCfg.gateway?.auth?.password,
timeoutMs,
json: opts.rpc.json,
})
: undefined;
let lastError: string | undefined;
if (
loaded &&
@@ -300,7 +438,9 @@ async function gatherDaemonStatus(opts: {
portStatus &&
portStatus.status !== "busy"
) {
lastError = (await readLastGatewayErrorLine(process.env)) ?? undefined;
lastError =
(await readLastGatewayErrorLine(mergedDaemonEnv as NodeJS.ProcessEnv)) ??
undefined;
}
return {
@@ -312,9 +452,23 @@ async function gatherDaemonStatus(opts: {
command,
runtime,
},
config: {
cli: cliConfigSummary,
daemon: daemonConfigSummary,
...(configMismatch ? { mismatch: true } : {}),
},
gateway: {
bindMode,
bindHost,
port: daemonPort,
portSource,
probeUrl,
...(probeNote ? { probeNote } : {}),
},
port: portStatus,
...(portCliStatus ? { portCli: portCliStatus } : {}),
lastError,
rpc,
...(rpc ? { rpc: { ...rpc, url: probeUrl } } : {}),
legacyServices,
extraServices,
};
@@ -341,9 +495,56 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
`Command: ${service.command.programArguments.join(" ")}`,
);
}
if (service.command?.sourcePath) {
defaultRuntime.log(`Service file: ${service.command.sourcePath}`);
}
if (service.command?.workingDirectory) {
defaultRuntime.log(`Working dir: ${service.command.workingDirectory}`);
}
const daemonEnvLines = safeDaemonEnv(service.command?.environment);
if (daemonEnvLines.length > 0) {
defaultRuntime.log(`Daemon env: ${daemonEnvLines.join(" ")}`);
}
if (status.config) {
const cliCfg = `${status.config.cli.path}${status.config.cli.exists ? "" : " (missing)"}${status.config.cli.valid ? "" : " (invalid)"}`;
defaultRuntime.log(`Config (cli): ${cliCfg}`);
if (!status.config.cli.valid && status.config.cli.issues?.length) {
for (const issue of status.config.cli.issues.slice(0, 5)) {
defaultRuntime.error(`Config issue: ${issue.path || "<root>"}: ${issue.message}`);
}
}
if (status.config.daemon) {
const daemonCfg = `${status.config.daemon.path}${status.config.daemon.exists ? "" : " (missing)"}${status.config.daemon.valid ? "" : " (invalid)"}`;
defaultRuntime.log(`Config (daemon): ${daemonCfg}`);
if (!status.config.daemon.valid && status.config.daemon.issues?.length) {
for (const issue of status.config.daemon.issues.slice(0, 5)) {
defaultRuntime.error(
`Daemon config issue: ${issue.path || "<root>"}: ${issue.message}`,
);
}
}
}
if (status.config.mismatch) {
defaultRuntime.error(
"Root cause: CLI and daemon are using different config paths (likely a profile/state-dir mismatch).",
);
}
}
if (status.gateway) {
const bindHost = status.gateway.bindHost ?? "n/a";
defaultRuntime.log(
`Gateway: bind=${status.gateway.bindMode} (${bindHost}), port=${status.gateway.port} (${status.gateway.portSource})`,
);
defaultRuntime.log(`Probe target: ${status.gateway.probeUrl}`);
if (status.gateway.probeNote) {
defaultRuntime.log(`Probe note: ${status.gateway.probeNote}`);
}
if (status.gateway.bindMode === "tailnet" && !status.gateway.bindHost) {
defaultRuntime.error(
"Root cause: gateway bind=tailnet but no tailnet interface was found.",
);
}
}
const runtimeLine = formatRuntimeStatus(service.runtime);
if (runtimeLine) {
defaultRuntime.log(`Runtime: ${runtimeLine}`);
@@ -352,7 +553,12 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
if (rpc.ok) {
defaultRuntime.log("RPC probe: ok");
} else {
defaultRuntime.error(`RPC probe: failed (${rpc.error})`);
defaultRuntime.error("RPC probe: failed");
if (rpc.url) defaultRuntime.error(`RPC target: ${rpc.url}`);
const lines = String(rpc.error ?? "unknown").split(/\r?\n/).filter(Boolean);
for (const line of lines.slice(0, 12)) {
defaultRuntime.error(` ${line}`);
}
}
}
if (service.runtime?.missingUnit) {
@@ -364,7 +570,10 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
defaultRuntime.error(
"Service is loaded but not running (likely exited immediately).",
);
for (const hint of renderRuntimeHints(service.runtime)) {
for (const hint of renderRuntimeHints(
service.runtime,
(service.command?.environment ?? process.env) as NodeJS.ProcessEnv,
)) {
defaultRuntime.error(hint);
}
}
@@ -384,6 +593,23 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
defaultRuntime.error(line);
}
}
if (status.port) {
const addrs = Array.from(
new Set(
status.port.listeners
.map((l) => l.address?.trim())
.filter((v): v is string => Boolean(v)),
),
);
if (addrs.length > 0) {
defaultRuntime.log(`Listening: ${addrs.join(", ")}`);
}
}
if (status.portCli && status.portCli.port !== status.port?.port) {
defaultRuntime.log(
`Note: CLI config resolves gateway port=${status.portCli.port} (${status.portCli.status}).`,
);
}
if (
service.loaded &&
service.runtime?.status === "running" &&
@@ -401,7 +627,9 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
`Logs: journalctl --user -u ${GATEWAY_SYSTEMD_SERVICE_NAME}.service -n 200 --no-pager`,
);
} else if (process.platform === "darwin") {
const logs = resolveGatewayLogPaths(process.env);
const logs = resolveGatewayLogPaths(
(service.command?.environment ?? process.env) as NodeJS.ProcessEnv,
);
defaultRuntime.error(`Logs: ${logs.stdoutPath}`);
defaultRuntime.error(`Errors: ${logs.stderrPath}`);
}
@@ -503,6 +731,10 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
});
const environment: Record<string, string | undefined> = {
PATH: process.env.PATH,
CLAWDBOT_PROFILE: process.env.CLAWDBOT_PROFILE,
CLAWDBOT_STATE_DIR: process.env.CLAWDBOT_STATE_DIR,
CLAWDBOT_CONFIG_PATH: process.env.CLAWDBOT_CONFIG_PATH,
CLAWDBOT_GATEWAY_PORT: String(port),
CLAWDBOT_GATEWAY_TOKEN:
opts.token ||
cfg.gateway?.auth?.token ||

View File

@@ -14,6 +14,7 @@ import {
} from "../daemon/constants.js";
import { resolveGatewayService } from "../daemon/service.js";
import { callGateway } from "../gateway/call.js";
import { resolveGatewayAuth } from "../gateway/auth.js";
import { startGatewayServer } from "../gateway/server.js";
import {
type GatewayWsLogStyle,
@@ -413,19 +414,20 @@ export function registerGatewayCli(program: Command) {
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 authConfig = {
...cfg.gateway?.auth,
...(authMode ? { mode: authMode } : {}),
...(opts.password ? { password: String(opts.password) } : {}),
...(opts.token ? { token: String(opts.token) } : {}),
};
const resolvedAuth = resolveGatewayAuth({
authConfig,
env: process.env,
tailscaleMode: tailscaleMode ?? cfg.gateway?.tailscale?.mode ?? "off",
});
const resolvedAuthMode = resolvedAuth.mode;
const tokenValue = resolvedAuth.token;
const passwordValue = resolvedAuth.password;
const authHints: string[] = [];
if (miskeys.hasGatewayToken) {
authHints.push(
@@ -484,9 +486,10 @@ export function registerGatewayCli(program: Command) {
await startGatewayServer(port, {
bind,
auth:
authMode || opts.password || authModeRaw
authMode || opts.password || opts.token || authModeRaw
? {
mode: authMode ?? undefined,
token: opts.token ? String(opts.token) : undefined,
password: opts.password
? String(opts.password)
: undefined,

View File

@@ -9,6 +9,7 @@ import {
writeConfigFile,
} from "../config/config.js";
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
import { readLastGatewayErrorLine } from "../daemon/diagnostics.js";
import { resolveGatewayService } from "../daemon/service.js";
import { buildGatewayConnectionDetails } from "../gateway/call.js";
import { formatPortDiagnostics, inspectPortUsage } from "../infra/ports.js";
@@ -216,21 +217,31 @@ export async function doctorCommand(
}
if (!healthOk) {
const service = resolveGatewayService();
const loaded = await service.isLoaded({ env: process.env });
let serviceRuntime:
| Awaited<ReturnType<typeof service.readRuntime>>
| undefined;
if (loaded) {
serviceRuntime = await service
.readRuntime(process.env)
.catch(() => undefined);
}
if (resolveMode(cfg) === "local") {
const port = resolveGatewayPort(cfg, process.env);
const diagnostics = await inspectPortUsage(port);
if (diagnostics.status === "busy") {
note(formatPortDiagnostics(diagnostics).join("\n"), "Gateway port");
} else if (loaded && serviceRuntime?.status === "running") {
const lastError = await readLastGatewayErrorLine(process.env);
if (lastError) {
note(`Last gateway error: ${lastError}`, "Gateway");
}
}
}
const service = resolveGatewayService();
const loaded = await service.isLoaded({ env: process.env });
if (!loaded) {
note("Gateway daemon not installed.", "Gateway");
} else {
const serviceRuntime = await service
.readRuntime(process.env)
.catch(() => undefined);
const summary = formatGatewayRuntimeSummary(serviceRuntime);
const hints = buildGatewayRuntimeHints(serviceRuntime, {
platform: process.platform,

View File

@@ -710,6 +710,30 @@ describe("legacy config detection", () => {
}
});
it("rejects gateway.token", async () => {
vi.resetModules();
const { validateConfigObject } = await import("./config.js");
const res = validateConfigObject({
gateway: { token: "legacy-token" },
});
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.issues[0]?.path).toBe("gateway.token");
}
});
it("migrates gateway.token to gateway.auth.token", async () => {
vi.resetModules();
const { migrateLegacyConfig } = await import("./config.js");
const res = migrateLegacyConfig({
gateway: { token: "legacy-token" },
});
expect(res.changes).toContain("Moved gateway.token → gateway.auth.token.");
expect(res.config?.gateway?.auth?.token).toBe("legacy-token");
expect(res.config?.gateway?.auth?.mode).toBe("token");
expect((res.config?.gateway as { token?: string })?.token).toBeUndefined();
});
it('rejects telegram.dmPolicy="open" without allowFrom "*"', async () => {
vi.resetModules();
const { validateConfigObject } = await import("./config.js");

View File

@@ -60,6 +60,11 @@ const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [
message:
"agent.imageModelFallbacks was replaced by agent.imageModel.fallbacks (run `clawdbot doctor` to migrate).",
},
{
path: ["gateway", "token"],
message:
"gateway.token is ignored; use gateway.auth.token instead (run `clawdbot doctor` to migrate).",
},
];
const LEGACY_CONFIG_MIGRATIONS: LegacyConfigMigration[] = [
@@ -154,6 +159,34 @@ const LEGACY_CONFIG_MIGRATIONS: LegacyConfigMigration[] = [
}
},
},
{
id: "gateway.token->gateway.auth.token",
describe: "Move gateway.token to gateway.auth.token",
apply: (raw, changes) => {
const gateway = raw.gateway;
if (!gateway || typeof gateway !== "object") return;
const token = (gateway as Record<string, unknown>).token;
if (token === undefined) return;
const gatewayObj = gateway as Record<string, unknown>;
const auth =
gatewayObj.auth && typeof gatewayObj.auth === "object"
? (gatewayObj.auth as Record<string, unknown>)
: {};
if (auth.token === undefined) {
auth.token = token;
if (!auth.mode) auth.mode = "token";
changes.push("Moved gateway.token → gateway.auth.token.");
} else {
changes.push("Removed gateway.token (gateway.auth.token already set).");
}
delete gatewayObj.token;
if (Object.keys(auth).length > 0) {
gatewayObj.auth = auth;
}
raw.gateway = gatewayObj;
},
},
{
id: "telegram.requireMention->telegram.groups.*.requireMention",
describe:

45
src/daemon/diagnostics.ts Normal file
View File

@@ -0,0 +1,45 @@
import fs from "node:fs/promises";
import { resolveGatewayLogPaths } from "./launchd.js";
const GATEWAY_LOG_ERROR_PATTERNS = [
/refusing to bind gateway/i,
/gateway auth mode/i,
/gateway start blocked/i,
/failed to bind gateway socket/i,
/tailscale .* requires/i,
];
async function readLastLogLine(filePath: string): Promise<string | null> {
try {
const raw = await fs.readFile(filePath, "utf8");
const lines = raw.split(/\r?\n/).map((line) => line.trim());
for (let i = lines.length - 1; i >= 0; i -= 1) {
if (lines[i]) return lines[i];
}
return null;
} catch {
return null;
}
}
export async function readLastGatewayErrorLine(
env: NodeJS.ProcessEnv,
): Promise<string | null> {
const { stdoutPath, stderrPath } = resolveGatewayLogPaths(env);
const stderrRaw = await fs.readFile(stderrPath, "utf8").catch(() => "");
const stdoutRaw = await fs.readFile(stdoutPath, "utf8").catch(() => "");
const lines = [...stderrRaw.split(/\r?\n/), ...stdoutRaw.split(/\r?\n/)].map(
(line) => line.trim(),
);
for (let i = lines.length - 1; i >= 0; i -= 1) {
const line = lines[i];
if (!line) continue;
if (GATEWAY_LOG_ERROR_PATTERNS.some((pattern) => pattern.test(line))) {
return line;
}
}
return (
(await readLastLogLine(stderrPath)) ?? (await readLastLogLine(stdoutPath))
);
}

View File

@@ -1,5 +1,6 @@
import { timingSafeEqual } from "node:crypto";
import type { IncomingMessage } from "node:http";
import type { GatewayAuthConfig, GatewayTailscaleMode } from "../config/config.js";
export type ResolvedGatewayAuthMode = "none" | "token" | "password";
export type ResolvedGatewayAuth = {
@@ -98,6 +99,29 @@ function isTailscaleProxyRequest(req?: IncomingMessage): boolean {
);
}
export function resolveGatewayAuth(params: {
authConfig?: GatewayAuthConfig | null;
env?: NodeJS.ProcessEnv;
tailscaleMode?: GatewayTailscaleMode;
}): ResolvedGatewayAuth {
const authConfig = params.authConfig ?? {};
const env = params.env ?? process.env;
const token = authConfig.token ?? env.CLAWDBOT_GATEWAY_TOKEN ?? undefined;
const password =
authConfig.password ?? env.CLAWDBOT_GATEWAY_PASSWORD ?? undefined;
const mode: ResolvedGatewayAuth["mode"] =
authConfig.mode ?? (password ? "password" : token ? "token" : "none");
const allowTailscale =
authConfig.allowTailscale ??
(params.tailscaleMode === "serve" && mode !== "password");
return {
mode,
token,
password,
allowTailscale,
};
}
export function assertGatewayAuthConfigured(auth: ResolvedGatewayAuth): void {
if (auth.mode === "token" && !auth.token) {
throw new Error(

View File

@@ -102,6 +102,7 @@ import type { WizardSession } from "../wizard/session.js";
import {
assertGatewayAuthConfigured,
authorizeGatewayConnect,
resolveGatewayAuth,
type ResolvedGatewayAuth,
} from "./auth.js";
import {
@@ -432,21 +433,12 @@ export async function startGatewayServer(
...tailscaleOverrides,
};
const tailscaleMode = tailscaleConfig.mode ?? "off";
const token =
authConfig.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN ?? undefined;
const password =
authConfig.password ?? process.env.CLAWDBOT_GATEWAY_PASSWORD ?? undefined;
const authMode: ResolvedGatewayAuth["mode"] =
authConfig.mode ?? (password ? "password" : token ? "token" : "none");
const allowTailscale =
authConfig.allowTailscale ??
(tailscaleMode === "serve" && authMode !== "password");
const resolvedAuth: ResolvedGatewayAuth = {
mode: authMode,
token,
password,
allowTailscale,
};
const resolvedAuth = resolveGatewayAuth({
authConfig,
env: process.env,
tailscaleMode,
});
const authMode: ResolvedGatewayAuth["mode"] = resolvedAuth.mode;
let hooksConfig = resolveHooksConfig(cfgAtStart);
const canvasHostEnabled =
process.env.CLAWDBOT_SKIP_CANVAS_HOST !== "1" &&