fix(doctor): audit env-only gateway tokens

This commit is contained in:
Sebastian
2026-02-16 22:24:09 -05:00
parent df8f7ff1ab
commit 52b624ccae
3 changed files with 101 additions and 5 deletions

View File

@@ -71,6 +71,7 @@ Docs: https://docs.openclaw.ai
- Sessions/Maintenance: archive transcripts when pruning stale sessions, clean expired media in subdirectories, and purge old deleted transcript archives to prevent disk leaks. (#18538)
- Infra/Fetch: ensure foreign abort-signal listener cleanup never masks original fetch successes/failures, while still preventing detached-finally unhandled rejection noise in `wrapFetchWithAbortSignal`. Thanks @Jackten.
- Heartbeat: allow suppressing tool error warning payloads during heartbeat runs via a new heartbeat config flag. (#18497) Thanks @thewilloftheshadow.
- Heartbeat: include sender metadata (From/To/Provider) in heartbeat prompts so model context matches the delivery target. (#18532)
- Heartbeat/Telegram: strip configured `responsePrefix` before heartbeat ack detection (with boundary-safe matching) so prefixed `HEARTBEAT_OK` replies are correctly suppressed instead of leaking into DMs. (#18602)
## 2026.2.15

View File

@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
const mocks = vi.hoisted(() => ({
@@ -52,6 +52,10 @@ vi.mock("./daemon-install-helpers.js", () => ({
import { maybeRepairGatewayServiceConfig } from "./doctor-gateway-services.js";
describe("maybeRepairGatewayServiceConfig", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("treats gateway.auth.token as source of truth for service token repairs", async () => {
mocks.readCommand.mockResolvedValue({
programArguments: ["/usr/bin/node", "/usr/local/bin/openclaw", "gateway", "--port", "18789"],
@@ -114,4 +118,84 @@ describe("maybeRepairGatewayServiceConfig", () => {
);
expect(mocks.install).toHaveBeenCalledTimes(1);
});
it("uses OPENCLAW_GATEWAY_TOKEN when config token is missing", async () => {
const previousToken = process.env.OPENCLAW_GATEWAY_TOKEN;
process.env.OPENCLAW_GATEWAY_TOKEN = "env-token";
try {
mocks.readCommand.mockResolvedValue({
programArguments: [
"/usr/bin/node",
"/usr/local/bin/openclaw",
"gateway",
"--port",
"18789",
],
environment: {
OPENCLAW_GATEWAY_TOKEN: "stale-token",
},
});
mocks.auditGatewayServiceConfig.mockResolvedValue({
ok: false,
issues: [
{
code: "gateway-token-mismatch",
message: "Gateway service OPENCLAW_GATEWAY_TOKEN does not match gateway.auth.token",
level: "recommended",
},
],
});
mocks.buildGatewayInstallPlan.mockResolvedValue({
programArguments: [
"/usr/bin/node",
"/usr/local/bin/openclaw",
"gateway",
"--port",
"18789",
],
workingDirectory: "/tmp",
environment: {
OPENCLAW_GATEWAY_TOKEN: "env-token",
},
});
mocks.install.mockResolvedValue(undefined);
const cfg: OpenClawConfig = {
gateway: {},
};
await maybeRepairGatewayServiceConfig(
cfg,
"local",
{ log: vi.fn(), error: vi.fn(), exit: vi.fn() },
{
confirm: vi.fn().mockResolvedValue(true),
confirmRepair: vi.fn().mockResolvedValue(true),
confirmAggressive: vi.fn().mockResolvedValue(true),
confirmSkipInNonInteractive: vi.fn().mockResolvedValue(true),
select: vi.fn().mockResolvedValue("node"),
shouldRepair: false,
shouldForce: false,
},
);
expect(mocks.auditGatewayServiceConfig).toHaveBeenCalledWith(
expect.objectContaining({
expectedGatewayToken: "env-token",
}),
);
expect(mocks.buildGatewayInstallPlan).toHaveBeenCalledWith(
expect.objectContaining({
token: "env-token",
}),
);
expect(mocks.install).toHaveBeenCalledTimes(1);
} finally {
if (previousToken === undefined) {
delete process.env.OPENCLAW_GATEWAY_TOKEN;
} else {
process.env.OPENCLAW_GATEWAY_TOKEN = previousToken;
}
}
});
});

View File

@@ -4,6 +4,8 @@ import os from "node:os";
import path from "node:path";
import { promisify } from "node:util";
import type { OpenClawConfig } from "../config/config.js";
import type { RuntimeEnv } from "../runtime.js";
import type { DoctorOptions, DoctorPrompter } from "./doctor-prompter.js";
import { resolveGatewayPort, resolveIsNixMode } from "../config/paths.js";
import { findExtraGatewayServices, renderGatewayServiceCleanupHints } from "../daemon/inspect.js";
import { renderSystemNodeWarning, resolveSystemNodeInfo } from "../daemon/runtime-paths.js";
@@ -13,11 +15,9 @@ import {
SERVICE_AUDIT_CODES,
} from "../daemon/service-audit.js";
import { resolveGatewayService } from "../daemon/service.js";
import type { RuntimeEnv } from "../runtime.js";
import { note } from "../terminal/note.js";
import { buildGatewayInstallPlan } from "./daemon-install-helpers.js";
import { DEFAULT_GATEWAY_DAEMON_RUNTIME, type GatewayDaemonRuntime } from "./daemon-runtime.js";
import type { DoctorOptions, DoctorPrompter } from "./doctor-prompter.js";
const execFileAsync = promisify(execFile);
@@ -50,6 +50,16 @@ function normalizeExecutablePath(value: string): string {
return path.resolve(value);
}
function resolveGatewayAuthToken(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): string | undefined {
const configToken = cfg.gateway?.auth?.token?.trim();
if (configToken) {
return configToken;
}
const envToken = env.OPENCLAW_GATEWAY_TOKEN ?? env.CLAWDBOT_GATEWAY_TOKEN;
const trimmedEnvToken = envToken?.trim();
return trimmedEnvToken || undefined;
}
function extractDetailPath(detail: string, prefix: string): string | null {
if (!detail.startsWith(prefix)) {
return null;
@@ -115,10 +125,11 @@ export async function maybeRepairGatewayServiceConfig(
return;
}
const expectedGatewayToken = resolveGatewayAuthToken(cfg, process.env);
const audit = await auditGatewayServiceConfig({
env: process.env,
command,
expectedGatewayToken: cfg.gateway?.auth?.token,
expectedGatewayToken,
});
const needsNodeRuntime = needsNodeRuntimeMigration(audit.issues);
const systemNodeInfo = needsNodeRuntime
@@ -141,7 +152,7 @@ export async function maybeRepairGatewayServiceConfig(
const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({
env: process.env,
port,
token: cfg.gateway?.auth?.token ?? process.env.OPENCLAW_GATEWAY_TOKEN,
token: expectedGatewayToken,
runtime: needsNodeRuntime && systemNodePath ? "node" : runtimeChoice,
nodePath: systemNodePath ?? undefined,
warn: (message, title) => note(message, title),