mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 06:14:23 +00:00
fix(doctor): audit env-only gateway tokens
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user