fix(daemon): scope token drift warnings

This commit is contained in:
Sebastian
2026-02-17 08:44:07 -05:00
parent 210bc37971
commit 111a24d55c
4 changed files with 117 additions and 21 deletions

View File

@@ -60,6 +60,7 @@ Docs: https://docs.openclaw.ai
- CLI/Doctor: ensure `openclaw doctor --fix --non-interactive --yes` exits promptly after completion so one-shot automation no longer hangs. (#18502)
- CLI/Doctor: auto-repair `dmPolicy="open"` configs missing wildcard allowlists and write channel-correct repair paths (including `channels.googlechat.dm.allowFrom`) so `openclaw doctor --fix` no longer leaves Google Chat configs invalid after attempted repair. (#18544)
- CLI/Doctor: detect gateway service token drift when the gateway token is only provided via environment variables, keeping service repairs aligned after token rotation.
- CLI/Daemon: warn when a gateway restart sees a stale service token so users can reinstall with `openclaw gateway install --force`, and skip drift warnings for non-gateway service restarts. (#18018)
- CLI/Status: fix `openclaw status --all` token summaries for bot-token-only channels so Mattermost/Zalo no longer show a bot+app warning. (#18527) Thanks @echo931.
- CLI/Configure: make the `/model picker` allowlist prompt searchable with tokenized matching in `openclaw configure` so users can filter huge model lists by typing terms like `gpt-5.2 openai/`. (#19010) Thanks @bjesuiter.
- Voice Call: add an optional stale call reaper (`staleCallReaperSeconds`) to end stuck calls when enabled. (#18437)

View File

@@ -0,0 +1,91 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const loadConfig = vi.fn(() => ({
gateway: {
auth: {
token: "config-token",
},
},
}));
const runtimeLogs: string[] = [];
const defaultRuntime = {
log: (message: string) => runtimeLogs.push(message),
error: vi.fn(),
exit: (code: number) => {
throw new Error(`__exit__:${code}`);
},
};
const service = {
label: "TestService",
loadedText: "loaded",
notLoadedText: "not loaded",
install: vi.fn(),
uninstall: vi.fn(),
stop: vi.fn(),
isLoaded: vi.fn(),
readCommand: vi.fn(),
readRuntime: vi.fn(),
restart: vi.fn(),
};
vi.mock("../../config/config.js", () => ({
loadConfig: () => loadConfig(),
}));
vi.mock("../../runtime.js", () => ({
defaultRuntime,
}));
describe("runServiceRestart token drift", () => {
beforeEach(() => {
runtimeLogs.length = 0;
loadConfig.mockClear();
service.isLoaded.mockClear();
service.readCommand.mockClear();
service.restart.mockClear();
service.isLoaded.mockResolvedValue(true);
service.readCommand.mockResolvedValue({
environment: { OPENCLAW_GATEWAY_TOKEN: "service-token" },
});
service.restart.mockResolvedValue(undefined);
vi.unstubAllEnvs();
vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", "");
vi.stubEnv("CLAWDBOT_GATEWAY_TOKEN", "");
});
it("emits drift warning when enabled", async () => {
const { runServiceRestart } = await import("./lifecycle-core.js");
await runServiceRestart({
serviceNoun: "Gateway",
service,
renderStartHints: () => [],
opts: { json: true },
checkTokenDrift: true,
});
expect(loadConfig).toHaveBeenCalledTimes(1);
const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{"));
const payload = JSON.parse(jsonLine ?? "{}") as { warnings?: string[] };
expect(payload.warnings?.[0]).toContain("gateway install --force");
});
it("skips drift warning when disabled", async () => {
const { runServiceRestart } = await import("./lifecycle-core.js");
await runServiceRestart({
serviceNoun: "Node",
service,
renderStartHints: () => [],
opts: { json: true },
});
expect(loadConfig).not.toHaveBeenCalled();
expect(service.readCommand).not.toHaveBeenCalled();
const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{"));
const payload = JSON.parse(jsonLine ?? "{}") as { warnings?: string[] };
expect(payload.warnings).toBeUndefined();
});
});

View File

@@ -236,6 +236,7 @@ export async function runServiceRestart(params: {
service: GatewayService;
renderStartHints: () => string[];
opts?: DaemonLifecycleOptions;
checkTokenDrift?: boolean;
}): Promise<boolean> {
const json = Boolean(params.opts?.json);
const { stdout, emit, fail } = createActionIO({ action: "restart", json });
@@ -259,31 +260,33 @@ export async function runServiceRestart(params: {
return false;
}
// Check for token drift before restart (service token vs config token)
const warnings: string[] = [];
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) {
const warning = driftIssue.detail
? `${driftIssue.message} ${driftIssue.detail}`
: driftIssue.message;
warnings.push(warning);
if (!json) {
defaultRuntime.log(`\n⚠ ${driftIssue.message}`);
if (driftIssue.detail) {
defaultRuntime.log(` ${driftIssue.detail}\n`);
if (params.checkTokenDrift) {
// 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) {
const warning = driftIssue.detail
? `${driftIssue.message} ${driftIssue.detail}`
: driftIssue.message;
warnings.push(warning);
if (!json) {
defaultRuntime.log(`\n⚠ ${driftIssue.message}`);
if (driftIssue.detail) {
defaultRuntime.log(` ${driftIssue.detail}\n`);
}
}
}
} catch {
// Non-fatal: token drift check is best-effort
}
} catch {
// Non-fatal: token drift check is best-effort
}
try {

View File

@@ -46,5 +46,6 @@ export async function runDaemonRestart(opts: DaemonLifecycleOptions = {}): Promi
service: resolveGatewayService(),
renderStartHints: renderGatewayServiceStartHints,
opts,
checkTokenDrift: true,
});
}