mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 18:41:24 +00:00
fix(daemon): warn on token drift during restart (#18018)
When the gateway token in config differs from the token embedded in the service plist/unit file, restart will not apply the new token. This can cause silent auth failures after OAuth token switches. Changes: - Add checkTokenDrift() to service-audit.ts - Call it in runServiceRestart() before restarting - Warn user with suggestion to run 'openclaw gateway install --force' Closes #18018
This commit is contained in:
committed by
Peter Steinberger
parent
8af4712c40
commit
d6e85aa6ba
@@ -1,5 +1,7 @@
|
||||
import type { GatewayService } from "../../daemon/service.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import { resolveIsNixMode } from "../../config/paths.js";
|
||||
import { checkTokenDrift } from "../../daemon/service-audit.js";
|
||||
import { renderSystemdUnavailableHints } from "../../daemon/systemd-hints.js";
|
||||
import { isSystemdUserServiceAvailable } from "../../daemon/systemd.js";
|
||||
import { isWSL } from "../../infra/wsl.js";
|
||||
@@ -255,6 +257,27 @@ export async function runServiceRestart(params: {
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for token drift before restart (service token vs config token)
|
||||
try {
|
||||
const command = await params.service.readCommand(process.env);
|
||||
const serviceToken = command?.environment?.OPENCLAW_GATEWAY_TOKEN;
|
||||
const cfg = loadConfig();
|
||||
const configToken =
|
||||
cfg.gateway?.auth?.token ||
|
||||
process.env.OPENCLAW_GATEWAY_TOKEN ||
|
||||
process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||
const driftIssue = checkTokenDrift({ serviceToken, configToken });
|
||||
if (driftIssue && !json) {
|
||||
defaultRuntime.log(`\n⚠️ ${driftIssue.message}`);
|
||||
if (driftIssue.detail) {
|
||||
defaultRuntime.log(` ${driftIssue.detail}\n`);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal: token drift check is best-effort
|
||||
}
|
||||
|
||||
try {
|
||||
await params.service.restart({ env: process.env, stdout });
|
||||
let restarted = true;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { auditGatewayServiceConfig, SERVICE_AUDIT_CODES } from "./service-audit.js";
|
||||
import { auditGatewayServiceConfig, checkTokenDrift, SERVICE_AUDIT_CODES } from "./service-audit.js";
|
||||
import { buildMinimalServicePath } from "./service-env.js";
|
||||
|
||||
describe("auditGatewayServiceConfig", () => {
|
||||
@@ -97,3 +97,39 @@ describe("auditGatewayServiceConfig", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkTokenDrift", () => {
|
||||
it("returns null when both tokens are undefined", () => {
|
||||
const result = checkTokenDrift({ serviceToken: undefined, configToken: undefined });
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when both tokens are empty strings", () => {
|
||||
const result = checkTokenDrift({ serviceToken: "", configToken: "" });
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when tokens match", () => {
|
||||
const result = checkTokenDrift({ serviceToken: "same-token", configToken: "same-token" });
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("detects drift when config has token but service has different token", () => {
|
||||
const result = checkTokenDrift({ serviceToken: "old-token", configToken: "new-token" });
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.code).toBe(SERVICE_AUDIT_CODES.gatewayTokenDrift);
|
||||
expect(result?.message).toContain("differs from service token");
|
||||
});
|
||||
|
||||
it("detects drift when config has token but service has no token", () => {
|
||||
const result = checkTokenDrift({ serviceToken: undefined, configToken: "new-token" });
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.code).toBe(SERVICE_AUDIT_CODES.gatewayTokenDrift);
|
||||
});
|
||||
|
||||
it("returns null when service has token but config does not", () => {
|
||||
// This is not really drift - service will work, just config is incomplete
|
||||
const result = checkTokenDrift({ serviceToken: "service-token", configToken: undefined });
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ export const SERVICE_AUDIT_CODES = {
|
||||
gatewayRuntimeBun: "gateway-runtime-bun",
|
||||
gatewayRuntimeNodeVersionManager: "gateway-runtime-node-version-manager",
|
||||
gatewayRuntimeNodeSystemMissing: "gateway-runtime-node-system-missing",
|
||||
gatewayTokenDrift: "gateway-token-drift",
|
||||
launchdKeepAlive: "launchd-keep-alive",
|
||||
launchdRunAtLoad: "launchd-run-at-load",
|
||||
systemdAfterNetworkOnline: "systemd-after-network-online",
|
||||
@@ -360,6 +361,35 @@ async function auditGatewayRuntime(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the service's embedded token differs from the config file token.
|
||||
* Returns an issue if drift is detected (service will use old token after restart).
|
||||
*/
|
||||
export function checkTokenDrift(params: {
|
||||
serviceToken: string | undefined;
|
||||
configToken: string | undefined;
|
||||
}): ServiceConfigIssue | null {
|
||||
const { serviceToken, configToken } = params;
|
||||
|
||||
// No drift if both are undefined/empty
|
||||
if (!serviceToken && !configToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Drift: config has token, service has different or no token
|
||||
if (configToken && serviceToken !== configToken) {
|
||||
return {
|
||||
code: SERVICE_AUDIT_CODES.gatewayTokenDrift,
|
||||
message:
|
||||
"Config token differs from service token. The daemon will use the old token after restart.",
|
||||
detail: "Run `openclaw gateway install --force` to sync the token.",
|
||||
level: "recommended",
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function auditGatewayServiceConfig(params: {
|
||||
env: Record<string, string | undefined>;
|
||||
command: GatewayServiceCommand;
|
||||
|
||||
Reference in New Issue
Block a user