fix: tighten gateway bind auth diagnostics
This commit is contained in:
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- CLI/Daemon: add `clawdbot logs` tailing and improve restart/service hints across platforms.
|
- CLI/Daemon: add `clawdbot logs` tailing and improve restart/service hints across platforms.
|
||||||
|
- Gateway/CLI: tighten LAN bind auth checks, warn on mis-keyed gateway tokens, and surface last gateway error when daemon looks running but the port is closed.
|
||||||
- Auto-reply: keep typing indicators alive during tool execution without changing typing-mode semantics. Thanks @thesash for PR #452.
|
- Auto-reply: keep typing indicators alive during tool execution without changing typing-mode semantics. Thanks @thesash for PR #452.
|
||||||
- macOS: harden Voice Wake tester/runtime (pause trigger, mic persistence, local-only tester) and keep transcript logs private. Thanks @xadenryan for PR #438.
|
- macOS: harden Voice Wake tester/runtime (pause trigger, mic persistence, local-only tester) and keep transcript logs private. Thanks @xadenryan for PR #438.
|
||||||
- macOS: preserve node bridge tunnel port override so remote nodes connect on the bridge port. Thanks @sircrumpet for PR #364.
|
- macOS: preserve node bridge tunnel port override so remote nodes connect on the bridge port. Thanks @sircrumpet for PR #364.
|
||||||
|
|||||||
@@ -1555,6 +1555,8 @@ Notes:
|
|||||||
- `clawdbot gateway` refuses to start unless `gateway.mode` is set to `local` (or you pass the override flag).
|
- `clawdbot gateway` refuses to start unless `gateway.mode` is set to `local` (or you pass the override flag).
|
||||||
- `gateway.port` controls the single multiplexed port used for WebSocket + HTTP (control UI, hooks, A2UI).
|
- `gateway.port` controls the single multiplexed port used for WebSocket + HTTP (control UI, hooks, A2UI).
|
||||||
- Precedence: `--port` > `CLAWDBOT_GATEWAY_PORT` > `gateway.port` > default `18789`.
|
- Precedence: `--port` > `CLAWDBOT_GATEWAY_PORT` > `gateway.port` > default `18789`.
|
||||||
|
- Non-loopback binds (`lan`/`tailnet`/`auto`) require auth. Use `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`).
|
||||||
|
- `gateway.remote.token` is **only** for remote CLI calls; it does not enable local gateway auth. `gateway.token` is ignored.
|
||||||
|
|
||||||
Auth and Tailscale:
|
Auth and Tailscale:
|
||||||
- `gateway.auth.mode` sets the handshake requirements (`token` or `password`).
|
- `gateway.auth.mode` sets the handshake requirements (`token` or `password`).
|
||||||
|
|||||||
@@ -29,6 +29,18 @@ Doctor/daemon will show runtime state (PID/last exit) and log hints.
|
|||||||
- Linux systemd (if installed): `journalctl --user -u clawdbot-gateway.service -n 200 --no-pager`
|
- Linux systemd (if installed): `journalctl --user -u clawdbot-gateway.service -n 200 --no-pager`
|
||||||
- Windows: `schtasks /Query /TN "Clawdbot Gateway" /V /FO LIST`
|
- Windows: `schtasks /Query /TN "Clawdbot Gateway" /V /FO LIST`
|
||||||
|
|
||||||
|
### Service Running but Port Not Listening
|
||||||
|
|
||||||
|
If the service reports **running** but nothing is listening on the gateway port,
|
||||||
|
the Gateway likely refused to bind.
|
||||||
|
|
||||||
|
**Check:**
|
||||||
|
- `gateway.mode` must be `local` for `clawdbot gateway` and the daemon.
|
||||||
|
- Non-loopback binds (`lan`/`tailnet`/`auto`) require auth:
|
||||||
|
`gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`).
|
||||||
|
- `gateway.remote.token` is for remote CLI calls only; it does **not** enable local auth.
|
||||||
|
- `gateway.token` is ignored; use `gateway.auth.token`.
|
||||||
|
|
||||||
### Address Already in Use (Port 18789)
|
### Address Already in Use (Port 18789)
|
||||||
|
|
||||||
This means something is already listening on the gateway port.
|
This means something is already listening on the gateway port.
|
||||||
|
|||||||
@@ -129,6 +129,10 @@ For daemon runs, persist it in `~/.clawdbot/clawdbot.json`:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Non-loopback binds require `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`).
|
||||||
|
- `gateway.remote.token` is only for remote CLI calls; it does not enable local auth.
|
||||||
|
|
||||||
Then point exe.dev’s proxy at `8080` (or whatever port you chose) and open your VM’s HTTPS URL:
|
Then point exe.dev’s proxy at `8080` (or whatever port you chose) and open your VM’s HTTPS URL:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ import {
|
|||||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||||
isGatewayDaemonRuntime,
|
isGatewayDaemonRuntime,
|
||||||
} from "../commands/daemon-runtime.js";
|
} 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 { resolveIsNixMode } from "../config/paths.js";
|
||||||
import {
|
import {
|
||||||
GATEWAY_LAUNCH_AGENT_LABEL,
|
GATEWAY_LAUNCH_AGENT_LABEL,
|
||||||
@@ -17,11 +23,13 @@ import {
|
|||||||
findExtraGatewayServices,
|
findExtraGatewayServices,
|
||||||
renderGatewayServiceCleanupHints,
|
renderGatewayServiceCleanupHints,
|
||||||
} from "../daemon/inspect.js";
|
} from "../daemon/inspect.js";
|
||||||
|
import { readLastGatewayErrorLine } from "../daemon/diagnostics.js";
|
||||||
import { resolveGatewayLogPaths } from "../daemon/launchd.js";
|
import { resolveGatewayLogPaths } from "../daemon/launchd.js";
|
||||||
import { findLegacyGatewayServices } from "../daemon/legacy.js";
|
import { findLegacyGatewayServices } from "../daemon/legacy.js";
|
||||||
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
||||||
import { resolveGatewayService } from "../daemon/service.js";
|
import { resolveGatewayService } from "../daemon/service.js";
|
||||||
import { callGateway } from "../gateway/call.js";
|
import { callGateway } from "../gateway/call.js";
|
||||||
|
import { resolveGatewayBindHost } from "../gateway/net.js";
|
||||||
import {
|
import {
|
||||||
formatPortDiagnostics,
|
formatPortDiagnostics,
|
||||||
inspectPortUsage,
|
inspectPortUsage,
|
||||||
@@ -33,6 +41,22 @@ import { defaultRuntime } from "../runtime.js";
|
|||||||
import { createDefaultDeps } from "./deps.js";
|
import { createDefaultDeps } from "./deps.js";
|
||||||
import { withProgress } from "./progress.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 = {
|
type DaemonStatus = {
|
||||||
service: {
|
service: {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -42,6 +66,8 @@ type DaemonStatus = {
|
|||||||
command?: {
|
command?: {
|
||||||
programArguments: string[];
|
programArguments: string[];
|
||||||
workingDirectory?: string;
|
workingDirectory?: string;
|
||||||
|
environment?: Record<string, string>;
|
||||||
|
sourcePath?: string;
|
||||||
} | null;
|
} | null;
|
||||||
runtime?: {
|
runtime?: {
|
||||||
status?: string;
|
status?: string;
|
||||||
@@ -57,15 +83,29 @@ type DaemonStatus = {
|
|||||||
missingUnit?: boolean;
|
missingUnit?: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
config?: {
|
||||||
|
cli: ConfigSummary;
|
||||||
|
daemon?: ConfigSummary;
|
||||||
|
mismatch?: boolean;
|
||||||
|
};
|
||||||
|
gateway?: GatewayStatusSummary;
|
||||||
port?: {
|
port?: {
|
||||||
port: number;
|
port: number;
|
||||||
status: PortUsageStatus;
|
status: PortUsageStatus;
|
||||||
listeners: PortListener[];
|
listeners: PortListener[];
|
||||||
hints: string[];
|
hints: string[];
|
||||||
};
|
};
|
||||||
|
portCli?: {
|
||||||
|
port: number;
|
||||||
|
status: PortUsageStatus;
|
||||||
|
listeners: PortListener[];
|
||||||
|
hints: string[];
|
||||||
|
};
|
||||||
|
lastError?: string;
|
||||||
rpc?: {
|
rpc?: {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
url?: string;
|
||||||
};
|
};
|
||||||
legacyServices: Array<{ label: string; detail: string }>;
|
legacyServices: Array<{ label: string; detail: string }>;
|
||||||
extraServices: Array<{ label: string; detail: string; scope: string }>;
|
extraServices: Array<{ label: string; detail: string; scope: string }>;
|
||||||
@@ -253,6 +293,15 @@ async function gatherDaemonStatus(opts: {
|
|||||||
deep: opts.deep,
|
deep: opts.deep,
|
||||||
});
|
});
|
||||||
const rpc = opts.probe ? await probeGatewayStatus(opts.rpc) : undefined;
|
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 {
|
return {
|
||||||
service: {
|
service: {
|
||||||
@@ -264,6 +313,7 @@ async function gatherDaemonStatus(opts: {
|
|||||||
runtime,
|
runtime,
|
||||||
},
|
},
|
||||||
port: portStatus,
|
port: portStatus,
|
||||||
|
lastError,
|
||||||
rpc,
|
rpc,
|
||||||
legacyServices,
|
legacyServices,
|
||||||
extraServices,
|
extraServices,
|
||||||
@@ -334,6 +384,28 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
|
|||||||
defaultRuntime.error(line);
|
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) {
|
if (legacyServices.length > 0) {
|
||||||
defaultRuntime.error("Legacy Clawdis services detected:");
|
defaultRuntime.error("Legacy Clawdis services detected:");
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { Command } from "commander";
|
|||||||
import {
|
import {
|
||||||
CONFIG_PATH_CLAWDBOT,
|
CONFIG_PATH_CLAWDBOT,
|
||||||
loadConfig,
|
loadConfig,
|
||||||
|
readConfigFileSnapshot,
|
||||||
resolveGatewayPort,
|
resolveGatewayPort,
|
||||||
} from "../config/config.js";
|
} from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
@@ -70,6 +71,26 @@ function describeUnknownError(err: unknown): string {
|
|||||||
return "Unknown error";
|
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[] {
|
function renderGatewayServiceStopHints(): string[] {
|
||||||
switch (process.platform) {
|
switch (process.platform) {
|
||||||
case "darwin":
|
case "darwin":
|
||||||
@@ -368,7 +389,7 @@ export function registerGatewayCli(program: Command) {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
defaultRuntime.error(
|
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);
|
defaultRuntime.exit(1);
|
||||||
@@ -390,6 +411,72 @@ export function registerGatewayCli(program: Command) {
|
|||||||
return;
|
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 {
|
try {
|
||||||
await runGatewayLoop({
|
await runGatewayLoop({
|
||||||
runtime: defaultRuntime,
|
runtime: defaultRuntime,
|
||||||
|
|||||||
@@ -59,6 +59,20 @@ export type ConfigIoDeps = {
|
|||||||
logger?: Pick<typeof console, "error" | "warn">;
|
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 {
|
function resolveConfigPathForDeps(deps: Required<ConfigIoDeps>): string {
|
||||||
if (deps.configPath) return deps.configPath;
|
if (deps.configPath) return deps.configPath;
|
||||||
return resolveConfigPath(deps.env, resolveStateDir(deps.env, deps.homedir));
|
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 raw = deps.fs.readFileSync(configPath, "utf-8");
|
||||||
const parsed = deps.json5.parse(raw);
|
const parsed = deps.json5.parse(raw);
|
||||||
|
warnOnConfigMiskeys(parsed, deps.logger);
|
||||||
if (typeof parsed !== "object" || parsed === null) return {};
|
if (typeof parsed !== "object" || parsed === null) return {};
|
||||||
const validated = ClawdbotSchema.safeParse(parsed);
|
const validated = ClawdbotSchema.safeParse(parsed);
|
||||||
if (!validated.success) {
|
if (!validated.success) {
|
||||||
|
|||||||
@@ -464,7 +464,7 @@ export async function startGatewayServer(
|
|||||||
}
|
}
|
||||||
if (!isLoopbackHost(bindHost) && authMode === "none") {
|
if (!isLoopbackHost(bindHost) && authMode === "none") {
|
||||||
throw new Error(
|
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)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user