fix(gateway): auto-generate token during gateway install to prevent launchd restart loop (#13813)
When the gateway is installed as a macOS launch agent and no token is configured, the service enters an infinite restart loop because launchd does not inherit shell environment variables. Auto-generate a token during `gateway install` when auth mode is `token` and no token exists, matching the existing pattern in doctor.ts and configure.gateway.ts. The token is persisted to the config file and embedded in the plist EnvironmentVariables for belt-and-suspenders reliability. Relates-to: #5103, #2433, #1690, #7749
This commit is contained in:
@@ -4,9 +4,16 @@ import {
|
||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
isGatewayDaemonRuntime,
|
||||
} from "../../commands/daemon-runtime.js";
|
||||
import { loadConfig, resolveGatewayPort } from "../../config/config.js";
|
||||
import { randomToken } from "../../commands/onboard-helpers.js";
|
||||
import {
|
||||
loadConfig,
|
||||
readConfigFileSnapshot,
|
||||
resolveGatewayPort,
|
||||
writeConfigFile,
|
||||
} from "../../config/config.js";
|
||||
import { resolveIsNixMode } from "../../config/paths.js";
|
||||
import { resolveGatewayService } from "../../daemon/service.js";
|
||||
import { resolveGatewayAuth } from "../../gateway/auth.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { formatCliCommand } from "../command-format.js";
|
||||
import { buildDaemonServiceSnapshot, createNullWriter, emitDaemonActionJson } from "./response.js";
|
||||
@@ -93,10 +100,78 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve effective auth mode to determine if token auto-generation is needed.
|
||||
// Password-mode and Tailscale-only installs do not need a token.
|
||||
const resolvedAuth = resolveGatewayAuth({
|
||||
authConfig: cfg.gateway?.auth,
|
||||
tailscaleMode: cfg.gateway?.tailscale?.mode ?? "off",
|
||||
});
|
||||
const needsToken =
|
||||
resolvedAuth.mode === "token" && !resolvedAuth.token && !resolvedAuth.allowTailscale;
|
||||
|
||||
let token: string | undefined =
|
||||
opts.token ||
|
||||
cfg.gateway?.auth?.token ||
|
||||
process.env.OPENCLAW_GATEWAY_TOKEN ||
|
||||
process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||
|
||||
if (!token && needsToken) {
|
||||
token = randomToken();
|
||||
const warnMsg = "No gateway token found. Auto-generated one and saving to config.";
|
||||
if (json) {
|
||||
warnings.push(warnMsg);
|
||||
} else {
|
||||
defaultRuntime.log(warnMsg);
|
||||
}
|
||||
|
||||
// Persist to config file so the gateway reads it at runtime
|
||||
// (launchd does not inherit shell env vars, and CLI tools also
|
||||
// read gateway.auth.token from config for gateway calls).
|
||||
try {
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
if (snapshot.exists && !snapshot.valid) {
|
||||
// Config file exists but is corrupt/unparseable — don't risk overwriting.
|
||||
// Token is still embedded in the plist EnvironmentVariables.
|
||||
const msg = "Warning: config file exists but is invalid; skipping token persistence.";
|
||||
if (json) {
|
||||
warnings.push(msg);
|
||||
} else {
|
||||
defaultRuntime.log(msg);
|
||||
}
|
||||
} else {
|
||||
const baseConfig = snapshot.exists ? snapshot.config : {};
|
||||
if (!baseConfig.gateway?.auth?.token) {
|
||||
await writeConfigFile({
|
||||
...baseConfig,
|
||||
gateway: {
|
||||
...baseConfig.gateway,
|
||||
auth: {
|
||||
...baseConfig.gateway?.auth,
|
||||
mode: baseConfig.gateway?.auth?.mode ?? "token",
|
||||
token,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Another process wrote a token between loadConfig() and now.
|
||||
token = baseConfig.gateway.auth.token;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Non-fatal: token is still embedded in the plist EnvironmentVariables.
|
||||
const msg = `Warning: could not persist token to config: ${String(err)}`;
|
||||
if (json) {
|
||||
warnings.push(msg);
|
||||
} else {
|
||||
defaultRuntime.log(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({
|
||||
env: process.env,
|
||||
port,
|
||||
token: opts.token || cfg.gateway?.auth?.token || process.env.OPENCLAW_GATEWAY_TOKEN,
|
||||
token,
|
||||
runtime: runtimeRaw,
|
||||
warn: (message) => {
|
||||
if (json) {
|
||||
|
||||
Reference in New Issue
Block a user