mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:50:44 +00:00
fix(gateway): refresh stale embedded service tokens
Refresh loaded gateway service installs when the current service embeds stale gateway auth instead of returning already-installed, avoiding LaunchAgent token-mismatch loops after token rotation. Fixes #70752. Thanks @hyspacex. Co-authored-by: Harry Xie <harryhsieh963@yahoo.com>
This commit is contained in:
@@ -75,6 +75,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Gateway/install: refresh loaded gateway service installs when the current service embeds stale gateway auth instead of returning already-installed, avoiding LaunchAgent token-mismatch loops after token rotation. Fixes #70752. Thanks @hyspacex.
|
||||
- Update: ignore bundled plugin `.openclaw-install-stage` directories during global install verification and packaged dist pruning so leftover runtime-dep staging files do not turn successful updates into `unexpected packaged dist file` failures. Fixes #71752. Thanks @waynegault.
|
||||
- Gateway/plugins: stop persisted WhatsApp auth state from activating bundled channel runtime-dependency repair during startup when `channels.whatsapp` is absent, avoiding npm/git stalls on packaged Linux installs. Fixes #71994. Thanks @xiao398008.
|
||||
- Gateway/device tokens: enforce caller-scope containment inside token rotation and revocation so pairing-only sessions cannot mutate higher-scope operator tokens. Fixes #71990. Thanks @coygeek.
|
||||
|
||||
@@ -361,6 +361,89 @@ describe("runDaemonInstall", () => {
|
||||
expect(actionState.emitted.at(-1)).toMatchObject({ result: "already-installed" });
|
||||
});
|
||||
|
||||
it("reinstalls when the loaded service still embeds OPENCLAW_GATEWAY_TOKEN", async () => {
|
||||
service.isLoaded.mockResolvedValue(true);
|
||||
service.readCommand.mockResolvedValue({
|
||||
programArguments: ["openclaw", "gateway", "run"],
|
||||
environment: {
|
||||
OPENCLAW_GATEWAY_TOKEN: "stale-service-token",
|
||||
},
|
||||
} as never);
|
||||
|
||||
await runDaemonInstall({ json: true });
|
||||
|
||||
expect(installDaemonServiceAndEmitMock).toHaveBeenCalledTimes(1);
|
||||
expect(actionState.warnings).toContain(
|
||||
"Gateway service OPENCLAW_GATEWAY_TOKEN differs from the current install plan; refreshing the install.",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns already-installed when the embedded gateway token matches the install plan", async () => {
|
||||
service.isLoaded.mockResolvedValue(true);
|
||||
service.readCommand.mockResolvedValue({
|
||||
programArguments: ["openclaw", "gateway", "run"],
|
||||
environment: {
|
||||
OPENCLAW_GATEWAY_TOKEN: "durable-token",
|
||||
},
|
||||
} as never);
|
||||
buildGatewayInstallPlanMock.mockResolvedValueOnce({
|
||||
programArguments: ["openclaw", "gateway", "run"],
|
||||
workingDirectory: "/tmp",
|
||||
environment: {
|
||||
OPENCLAW_GATEWAY_TOKEN: "durable-token",
|
||||
},
|
||||
});
|
||||
|
||||
await runDaemonInstall({ json: true });
|
||||
|
||||
expect(buildGatewayInstallPlanMock).toHaveBeenCalledTimes(1);
|
||||
expect(writeConfigFileMock).not.toHaveBeenCalled();
|
||||
expect(installDaemonServiceAndEmitMock).not.toHaveBeenCalled();
|
||||
expect(actionState.emitted.at(-1)).toMatchObject({ result: "already-installed" });
|
||||
});
|
||||
|
||||
it("reinstalls when the embedded gateway token differs from the install plan", async () => {
|
||||
service.isLoaded.mockResolvedValue(true);
|
||||
service.readCommand.mockResolvedValue({
|
||||
programArguments: ["openclaw", "gateway", "run"],
|
||||
environment: {
|
||||
OPENCLAW_GATEWAY_TOKEN: "stale-service-token",
|
||||
},
|
||||
} as never);
|
||||
buildGatewayInstallPlanMock.mockResolvedValueOnce({
|
||||
programArguments: ["openclaw", "gateway", "run"],
|
||||
workingDirectory: "/tmp",
|
||||
environment: {
|
||||
OPENCLAW_GATEWAY_TOKEN: "fresh-token",
|
||||
},
|
||||
});
|
||||
|
||||
await runDaemonInstall({ json: true });
|
||||
|
||||
expect(installDaemonServiceAndEmitMock).toHaveBeenCalledTimes(1);
|
||||
expect(actionState.warnings).toContain(
|
||||
"Gateway service OPENCLAW_GATEWAY_TOKEN differs from the current install plan; refreshing the install.",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not reinstall when OPENCLAW_GATEWAY_TOKEN comes from an env file", async () => {
|
||||
service.isLoaded.mockResolvedValue(true);
|
||||
service.readCommand.mockResolvedValue({
|
||||
programArguments: ["openclaw", "gateway", "run"],
|
||||
environment: {
|
||||
OPENCLAW_GATEWAY_TOKEN: "env-file-token",
|
||||
},
|
||||
environmentValueSources: {
|
||||
OPENCLAW_GATEWAY_TOKEN: "file",
|
||||
},
|
||||
} as never);
|
||||
|
||||
await runDaemonInstall({ json: true });
|
||||
|
||||
expect(installDaemonServiceAndEmitMock).not.toHaveBeenCalled();
|
||||
expect(actionState.emitted.at(-1)).toMatchObject({ result: "already-installed" });
|
||||
});
|
||||
|
||||
it("reinstalls when an existing service is missing the nvm TLS CA bundle", async () => {
|
||||
service.isLoaded.mockResolvedValue(true);
|
||||
resolveNodeStartupTlsEnvironmentMock.mockReturnValue({
|
||||
|
||||
@@ -3,12 +3,16 @@ import { buildGatewayInstallPlan } from "../../commands/daemon-install-helpers.j
|
||||
import {
|
||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
isGatewayDaemonRuntime,
|
||||
type GatewayDaemonRuntime,
|
||||
} from "../../commands/daemon-runtime.js";
|
||||
import { resolveGatewayInstallToken } from "../../commands/gateway-install-token.js";
|
||||
import { resolveFutureConfigActionBlock } from "../../config/future-version-guard.js";
|
||||
import { readConfigFileSnapshotForWrite } from "../../config/io.js";
|
||||
import { resolveGatewayPort } from "../../config/paths.js";
|
||||
import type { OpenClawConfig } from "../../config/types.js";
|
||||
import { readEmbeddedGatewayToken } from "../../daemon/service-audit.js";
|
||||
import { resolveGatewayService } from "../../daemon/service.js";
|
||||
import type { GatewayServiceCommandConfig } from "../../daemon/service.js";
|
||||
import { isNonFatalSystemdInstallProbeError } from "../../daemon/systemd.js";
|
||||
import {
|
||||
isDangerousHostEnvOverrideVarName,
|
||||
@@ -16,6 +20,7 @@ import {
|
||||
normalizeEnvVarKey,
|
||||
} from "../../infra/host-env-security.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
||||
import { formatCliCommand } from "../command-format.js";
|
||||
import { buildDaemonServiceSnapshot, installDaemonServiceAndEmit } from "./response.js";
|
||||
import {
|
||||
@@ -98,6 +103,7 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
const service = resolveGatewayService();
|
||||
let loaded = false;
|
||||
let existingServiceEnv: Record<string, string> | undefined;
|
||||
let existingServiceCommand: GatewayServiceCommandConfig | null = null;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env });
|
||||
} catch (err) {
|
||||
@@ -109,7 +115,8 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
}
|
||||
}
|
||||
if (loaded) {
|
||||
existingServiceEnv = (await service.readCommand(process.env).catch(() => null))?.environment;
|
||||
existingServiceCommand = await service.readCommand(process.env).catch(() => null);
|
||||
existingServiceEnv = existingServiceCommand?.environment;
|
||||
}
|
||||
const installEnv = mergeInstallInvocationEnv({
|
||||
env: process.env,
|
||||
@@ -117,12 +124,20 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
});
|
||||
if (loaded) {
|
||||
if (!opts.force) {
|
||||
if (await gatewayServiceNeedsAutoNodeExtraCaCertsRefresh({ service, env: process.env })) {
|
||||
const message = "Gateway service is missing the nvm TLS CA bundle; refreshing the install.";
|
||||
const autoRefreshMessage = await getGatewayServiceAutoRefreshMessage({
|
||||
currentCommand: existingServiceCommand,
|
||||
env: process.env,
|
||||
installEnv,
|
||||
port,
|
||||
runtime: runtimeRaw,
|
||||
existingEnvironment: existingServiceEnv,
|
||||
config: cfg,
|
||||
});
|
||||
if (autoRefreshMessage) {
|
||||
if (json) {
|
||||
warnings.push(message);
|
||||
warnings.push(autoRefreshMessage);
|
||||
} else {
|
||||
defaultRuntime.log(message);
|
||||
defaultRuntime.log(autoRefreshMessage);
|
||||
}
|
||||
} else {
|
||||
emit({
|
||||
@@ -196,18 +211,40 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
});
|
||||
}
|
||||
|
||||
async function gatewayServiceNeedsAutoNodeExtraCaCertsRefresh(params: {
|
||||
service: ReturnType<typeof resolveGatewayService>;
|
||||
async function getGatewayServiceAutoRefreshMessage(params: {
|
||||
currentCommand: GatewayServiceCommandConfig | null;
|
||||
env: Record<string, string | undefined>;
|
||||
}): Promise<boolean> {
|
||||
installEnv: NodeJS.ProcessEnv;
|
||||
port: number;
|
||||
runtime: GatewayDaemonRuntime;
|
||||
existingEnvironment?: Record<string, string | undefined>;
|
||||
config: OpenClawConfig;
|
||||
}): Promise<string | undefined> {
|
||||
try {
|
||||
const currentCommand = await params.service.readCommand(params.env);
|
||||
const currentCommand = params.currentCommand;
|
||||
if (!currentCommand) {
|
||||
return false;
|
||||
return undefined;
|
||||
}
|
||||
const currentEmbeddedToken = readEmbeddedGatewayToken(currentCommand);
|
||||
if (currentEmbeddedToken) {
|
||||
const plannedInstall = await buildGatewayInstallPlan({
|
||||
env: params.installEnv,
|
||||
port: params.port,
|
||||
runtime: params.runtime,
|
||||
existingEnvironment: params.existingEnvironment,
|
||||
warn: () => undefined,
|
||||
config: params.config,
|
||||
});
|
||||
const plannedEmbeddedToken = normalizeOptionalString(
|
||||
plannedInstall.environment.OPENCLAW_GATEWAY_TOKEN,
|
||||
);
|
||||
if (currentEmbeddedToken !== plannedEmbeddedToken) {
|
||||
return "Gateway service OPENCLAW_GATEWAY_TOKEN differs from the current install plan; refreshing the install.";
|
||||
}
|
||||
}
|
||||
const currentExecPath = currentCommand.programArguments[0]?.trim();
|
||||
if (!currentExecPath) {
|
||||
return false;
|
||||
return undefined;
|
||||
}
|
||||
const currentEnvironment = currentCommand.environment ?? {};
|
||||
const currentNodeExtraCaCerts = currentEnvironment.NODE_EXTRA_CA_CERTS?.trim();
|
||||
@@ -221,10 +258,13 @@ async function gatewayServiceNeedsAutoNodeExtraCaCertsRefresh(params: {
|
||||
includeDarwinDefaults: false,
|
||||
}).NODE_EXTRA_CA_CERTS;
|
||||
if (!expectedNodeExtraCaCerts) {
|
||||
return false;
|
||||
return undefined;
|
||||
}
|
||||
return currentNodeExtraCaCerts !== expectedNodeExtraCaCerts;
|
||||
if (currentNodeExtraCaCerts !== expectedNodeExtraCaCerts) {
|
||||
return "Gateway service is missing the nvm TLS CA bundle; refreshing the install.";
|
||||
}
|
||||
return undefined;
|
||||
} catch {
|
||||
return false;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user