mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-31 20:01:36 +00:00
fix(gateway): land #28428 from @l0cka
Landed from contributor PR #28428 by @l0cka. Co-authored-by: Daniel Alkurdi <danielalkurdi@gmail.com>
This commit is contained in:
@@ -82,11 +82,8 @@ describe("maybeInstallDaemon", () => {
|
||||
});
|
||||
|
||||
expect(resolveGatewayInstallToken).toHaveBeenCalledTimes(1);
|
||||
expect(buildGatewayInstallPlan).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
token: undefined,
|
||||
}),
|
||||
);
|
||||
expect(buildGatewayInstallPlan).toHaveBeenCalledTimes(1);
|
||||
expect("token" in buildGatewayInstallPlan.mock.calls[0][0]).toBe(false);
|
||||
expect(serviceInstall).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -112,7 +112,6 @@ export async function maybeInstallDaemon(params: {
|
||||
const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({
|
||||
env: process.env,
|
||||
port: params.port,
|
||||
token: tokenResolution.token,
|
||||
runtime: daemonRuntime,
|
||||
warn: (message, title) => note(message, title),
|
||||
config: cfg,
|
||||
|
||||
@@ -23,7 +23,6 @@ export async function buildGatewayInstallPlan(params: {
|
||||
env: Record<string, string | undefined>;
|
||||
port: number;
|
||||
runtime: GatewayDaemonRuntime;
|
||||
token?: string;
|
||||
devMode?: boolean;
|
||||
nodePath?: string;
|
||||
warn?: DaemonInstallWarnFn;
|
||||
@@ -52,7 +51,6 @@ export async function buildGatewayInstallPlan(params: {
|
||||
const serviceEnvironment = buildServiceEnvironment({
|
||||
env: params.env,
|
||||
port: params.port,
|
||||
token: params.token,
|
||||
launchdLabel:
|
||||
process.platform === "darwin"
|
||||
? resolveGatewayLaunchAgentLabel(params.env.OPENCLAW_PROFILE)
|
||||
|
||||
@@ -194,7 +194,6 @@ export async function maybeRepairGatewayDaemon(params: {
|
||||
const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({
|
||||
env: process.env,
|
||||
port,
|
||||
token: tokenResolution.token,
|
||||
runtime: daemonRuntime,
|
||||
warn: (message, title) => note(message, title),
|
||||
config: params.cfg,
|
||||
|
||||
@@ -5,9 +5,10 @@ import { withEnvAsync } from "../test-utils/env.js";
|
||||
const mocks = vi.hoisted(() => ({
|
||||
readCommand: vi.fn(),
|
||||
install: vi.fn(),
|
||||
writeConfigFile: vi.fn().mockResolvedValue(undefined),
|
||||
auditGatewayServiceConfig: vi.fn(),
|
||||
buildGatewayInstallPlan: vi.fn(),
|
||||
resolveGatewayInstallToken: vi.fn(),
|
||||
resolveGatewayAuthTokenForService: vi.fn(),
|
||||
resolveGatewayPort: vi.fn(() => 18789),
|
||||
resolveIsNixMode: vi.fn(() => false),
|
||||
findExtraGatewayServices: vi.fn().mockResolvedValue([]),
|
||||
@@ -21,6 +22,10 @@ vi.mock("../config/paths.js", () => ({
|
||||
resolveIsNixMode: mocks.resolveIsNixMode,
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
writeConfigFile: mocks.writeConfigFile,
|
||||
}));
|
||||
|
||||
vi.mock("../daemon/inspect.js", () => ({
|
||||
findExtraGatewayServices: mocks.findExtraGatewayServices,
|
||||
renderGatewayServiceCleanupHints: mocks.renderGatewayServiceCleanupHints,
|
||||
@@ -58,8 +63,8 @@ vi.mock("./daemon-install-helpers.js", () => ({
|
||||
buildGatewayInstallPlan: mocks.buildGatewayInstallPlan,
|
||||
}));
|
||||
|
||||
vi.mock("./gateway-install-token.js", () => ({
|
||||
resolveGatewayInstallToken: mocks.resolveGatewayInstallToken,
|
||||
vi.mock("./doctor-gateway-auth-token.js", () => ({
|
||||
resolveGatewayAuthTokenForService: mocks.resolveGatewayAuthTokenForService,
|
||||
}));
|
||||
|
||||
import {
|
||||
@@ -95,7 +100,7 @@ const gatewayProgramArguments = [
|
||||
"18789",
|
||||
];
|
||||
|
||||
function setupGatewayTokenRepairScenario(expectedToken: string) {
|
||||
function setupGatewayTokenRepairScenario() {
|
||||
mocks.readCommand.mockResolvedValue({
|
||||
programArguments: gatewayProgramArguments,
|
||||
environment: {
|
||||
@@ -115,14 +120,7 @@ function setupGatewayTokenRepairScenario(expectedToken: string) {
|
||||
mocks.buildGatewayInstallPlan.mockResolvedValue({
|
||||
programArguments: gatewayProgramArguments,
|
||||
workingDirectory: "/tmp",
|
||||
environment: {
|
||||
OPENCLAW_GATEWAY_TOKEN: expectedToken,
|
||||
},
|
||||
});
|
||||
mocks.resolveGatewayInstallToken.mockResolvedValue({
|
||||
token: expectedToken,
|
||||
tokenRefConfigured: false,
|
||||
warnings: [],
|
||||
environment: {},
|
||||
});
|
||||
mocks.install.mockResolvedValue(undefined);
|
||||
}
|
||||
@@ -130,10 +128,16 @@ function setupGatewayTokenRepairScenario(expectedToken: string) {
|
||||
describe("maybeRepairGatewayServiceConfig", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.resolveGatewayAuthTokenForService.mockImplementation(async (cfg: OpenClawConfig, env) => {
|
||||
const configToken =
|
||||
typeof cfg.gateway?.auth?.token === "string" ? cfg.gateway.auth.token.trim() : undefined;
|
||||
const envToken = env.OPENCLAW_GATEWAY_TOKEN?.trim() || undefined;
|
||||
return { token: configToken || envToken };
|
||||
});
|
||||
});
|
||||
|
||||
it("treats gateway.auth.token as source of truth for service token repairs", async () => {
|
||||
setupGatewayTokenRepairScenario("config-token");
|
||||
setupGatewayTokenRepairScenario();
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
gateway: {
|
||||
@@ -153,15 +157,22 @@ describe("maybeRepairGatewayServiceConfig", () => {
|
||||
);
|
||||
expect(mocks.buildGatewayInstallPlan).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
token: "config-token",
|
||||
config: expect.objectContaining({
|
||||
gateway: expect.objectContaining({
|
||||
auth: expect.objectContaining({
|
||||
token: "config-token",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
|
||||
expect(mocks.install).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("uses OPENCLAW_GATEWAY_TOKEN when config token is missing", async () => {
|
||||
await withEnvAsync({ OPENCLAW_GATEWAY_TOKEN: "env-token" }, async () => {
|
||||
setupGatewayTokenRepairScenario("env-token");
|
||||
setupGatewayTokenRepairScenario();
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
gateway: {},
|
||||
@@ -176,7 +187,22 @@ describe("maybeRepairGatewayServiceConfig", () => {
|
||||
);
|
||||
expect(mocks.buildGatewayInstallPlan).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
token: "env-token",
|
||||
config: expect.objectContaining({
|
||||
gateway: expect.objectContaining({
|
||||
auth: expect.objectContaining({
|
||||
token: "env-token",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(mocks.writeConfigFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
gateway: expect.objectContaining({
|
||||
auth: expect.objectContaining({
|
||||
token: "env-token",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(mocks.install).toHaveBeenCalledTimes(1);
|
||||
@@ -190,11 +216,6 @@ describe("maybeRepairGatewayServiceConfig", () => {
|
||||
OPENCLAW_GATEWAY_TOKEN: "stale-token",
|
||||
},
|
||||
});
|
||||
mocks.resolveGatewayInstallToken.mockResolvedValue({
|
||||
token: undefined,
|
||||
tokenRefConfigured: true,
|
||||
warnings: [],
|
||||
});
|
||||
mocks.auditGatewayServiceConfig.mockResolvedValue({
|
||||
ok: false,
|
||||
issues: [],
|
||||
@@ -228,11 +249,56 @@ describe("maybeRepairGatewayServiceConfig", () => {
|
||||
);
|
||||
expect(mocks.buildGatewayInstallPlan).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
token: undefined,
|
||||
config: cfg,
|
||||
}),
|
||||
);
|
||||
expect(mocks.install).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("falls back to embedded service token when config and env tokens are missing", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
OPENCLAW_GATEWAY_TOKEN: undefined,
|
||||
CLAWDBOT_GATEWAY_TOKEN: undefined,
|
||||
},
|
||||
async () => {
|
||||
setupGatewayTokenRepairScenario();
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
gateway: {},
|
||||
};
|
||||
|
||||
await runRepair(cfg);
|
||||
|
||||
expect(mocks.auditGatewayServiceConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
expectedGatewayToken: undefined,
|
||||
}),
|
||||
);
|
||||
expect(mocks.writeConfigFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
gateway: expect.objectContaining({
|
||||
auth: expect.objectContaining({
|
||||
token: "stale-token",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(mocks.buildGatewayInstallPlan).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
gateway: expect.objectContaining({
|
||||
auth: expect.objectContaining({
|
||||
token: "stale-token",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(mocks.install).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("maybeScanExtraGatewayServices", () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { writeConfigFile, type OpenClawConfig } from "../config/config.js";
|
||||
import { resolveGatewayPort, resolveIsNixMode } from "../config/paths.js";
|
||||
import { resolveSecretInputRef } from "../config/types.secrets.js";
|
||||
import {
|
||||
@@ -25,7 +25,6 @@ import { buildGatewayInstallPlan } from "./daemon-install-helpers.js";
|
||||
import { DEFAULT_GATEWAY_DAEMON_RUNTIME, type GatewayDaemonRuntime } from "./daemon-runtime.js";
|
||||
import { resolveGatewayAuthTokenForService } from "./doctor-gateway-auth-token.js";
|
||||
import type { DoctorOptions, DoctorPrompter } from "./doctor-prompter.js";
|
||||
import { resolveGatewayInstallToken } from "./gateway-install-token.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
@@ -259,24 +258,9 @@ export async function maybeRepairGatewayServiceConfig(
|
||||
|
||||
const port = resolveGatewayPort(cfg, process.env);
|
||||
const runtimeChoice = detectGatewayRuntime(command.programArguments);
|
||||
const installTokenResolution = await resolveGatewayInstallToken({
|
||||
config: cfg,
|
||||
env: process.env,
|
||||
});
|
||||
for (const warning of installTokenResolution.warnings) {
|
||||
note(warning, "Gateway service config");
|
||||
}
|
||||
if (installTokenResolution.unavailableReason) {
|
||||
note(
|
||||
`Unable to verify gateway service token drift: ${installTokenResolution.unavailableReason}`,
|
||||
"Gateway service config",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({
|
||||
const { programArguments } = await buildGatewayInstallPlan({
|
||||
env: process.env,
|
||||
port,
|
||||
token: installTokenResolution.token,
|
||||
runtime: needsNodeRuntime && systemNodePath ? "node" : runtimeChoice,
|
||||
nodePath: systemNodePath ?? undefined,
|
||||
warn: (message, title) => note(message, title),
|
||||
@@ -332,13 +316,56 @@ export async function maybeRepairGatewayServiceConfig(
|
||||
if (!repair) {
|
||||
return;
|
||||
}
|
||||
const serviceEmbeddedToken = command.environment?.OPENCLAW_GATEWAY_TOKEN?.trim() || undefined;
|
||||
const gatewayTokenForRepair = expectedGatewayToken ?? serviceEmbeddedToken;
|
||||
const configuredGatewayToken =
|
||||
typeof cfg.gateway?.auth?.token === "string"
|
||||
? cfg.gateway.auth.token.trim() || undefined
|
||||
: undefined;
|
||||
let cfgForServiceInstall = cfg;
|
||||
if (!tokenRefConfigured && !configuredGatewayToken && gatewayTokenForRepair) {
|
||||
const nextCfg: OpenClawConfig = {
|
||||
...cfg,
|
||||
gateway: {
|
||||
...cfg.gateway,
|
||||
auth: {
|
||||
...cfg.gateway?.auth,
|
||||
mode: cfg.gateway?.auth?.mode ?? "token",
|
||||
token: gatewayTokenForRepair,
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
await writeConfigFile(nextCfg);
|
||||
cfgForServiceInstall = nextCfg;
|
||||
note(
|
||||
expectedGatewayToken
|
||||
? "Persisted gateway.auth.token from environment before reinstalling service."
|
||||
: "Persisted gateway.auth.token from existing service definition before reinstalling service.",
|
||||
"Gateway",
|
||||
);
|
||||
} catch (err) {
|
||||
runtime.error(`Failed to persist gateway.auth.token before service repair: ${String(err)}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const updatedPort = resolveGatewayPort(cfgForServiceInstall, process.env);
|
||||
const updatedPlan = await buildGatewayInstallPlan({
|
||||
env: process.env,
|
||||
port: updatedPort,
|
||||
runtime: needsNodeRuntime && systemNodePath ? "node" : runtimeChoice,
|
||||
nodePath: systemNodePath ?? undefined,
|
||||
warn: (message, title) => note(message, title),
|
||||
config: cfgForServiceInstall,
|
||||
});
|
||||
try {
|
||||
await service.install({
|
||||
env: process.env,
|
||||
stdout: process.stdout,
|
||||
programArguments,
|
||||
workingDirectory,
|
||||
environment,
|
||||
programArguments: updatedPlan.programArguments,
|
||||
workingDirectory: updatedPlan.workingDirectory,
|
||||
environment: updatedPlan.environment,
|
||||
});
|
||||
} catch (err) {
|
||||
runtime.error(`Gateway service update failed: ${String(err)}`);
|
||||
|
||||
@@ -189,6 +189,8 @@ export async function resolveAuthForTarget(
|
||||
}
|
||||
return passwordResolution.value;
|
||||
};
|
||||
const withDiagnostics = <T extends { token?: string; password?: string }>(result: T) =>
|
||||
diagnostics.length > 0 ? { ...result, diagnostics } : result;
|
||||
|
||||
if (target.kind === "configRemote" || target.kind === "sshTunnel") {
|
||||
const remoteTokenValue = cfg.gateway?.remote?.token;
|
||||
@@ -198,11 +200,7 @@ export async function resolveAuthForTarget(
|
||||
const password = token
|
||||
? undefined
|
||||
: await resolvePassword(remotePasswordValue, "gateway.remote.password");
|
||||
return {
|
||||
token,
|
||||
password,
|
||||
...(diagnostics.length > 0 ? { diagnostics } : {}),
|
||||
};
|
||||
return withDiagnostics({ token, password });
|
||||
}
|
||||
|
||||
const authDisabled = authMode === "none" || authMode === "trusted-proxy";
|
||||
@@ -213,49 +211,39 @@ export async function resolveAuthForTarget(
|
||||
const envToken = readGatewayTokenEnv();
|
||||
const envPassword = readGatewayPasswordEnv();
|
||||
if (tokenOnly) {
|
||||
const token = await resolveToken(cfg.gateway?.auth?.token, "gateway.auth.token");
|
||||
if (token) {
|
||||
return withDiagnostics({ token });
|
||||
}
|
||||
if (envToken) {
|
||||
return { token: envToken };
|
||||
}
|
||||
const token = await resolveToken(cfg.gateway?.auth?.token, "gateway.auth.token");
|
||||
return {
|
||||
token,
|
||||
...(diagnostics.length > 0 ? { diagnostics } : {}),
|
||||
};
|
||||
return withDiagnostics({});
|
||||
}
|
||||
if (passwordOnly) {
|
||||
const password = await resolvePassword(cfg.gateway?.auth?.password, "gateway.auth.password");
|
||||
if (password) {
|
||||
return withDiagnostics({ password });
|
||||
}
|
||||
if (envPassword) {
|
||||
return { password: envPassword };
|
||||
}
|
||||
const password = await resolvePassword(cfg.gateway?.auth?.password, "gateway.auth.password");
|
||||
return {
|
||||
password,
|
||||
...(diagnostics.length > 0 ? { diagnostics } : {}),
|
||||
};
|
||||
return withDiagnostics({});
|
||||
}
|
||||
|
||||
const token = await resolveToken(cfg.gateway?.auth?.token, "gateway.auth.token");
|
||||
if (token) {
|
||||
return withDiagnostics({ token });
|
||||
}
|
||||
if (envToken) {
|
||||
return { token: envToken };
|
||||
}
|
||||
const token = await resolveToken(cfg.gateway?.auth?.token, "gateway.auth.token");
|
||||
if (token) {
|
||||
return {
|
||||
token,
|
||||
...(diagnostics.length > 0 ? { diagnostics } : {}),
|
||||
};
|
||||
}
|
||||
if (envPassword) {
|
||||
return {
|
||||
password: envPassword,
|
||||
...(diagnostics.length > 0 ? { diagnostics } : {}),
|
||||
};
|
||||
return withDiagnostics({ password: envPassword });
|
||||
}
|
||||
const password = await resolvePassword(cfg.gateway?.auth?.password, "gateway.auth.password");
|
||||
|
||||
return {
|
||||
token,
|
||||
password,
|
||||
...(diagnostics.length > 0 ? { diagnostics } : {}),
|
||||
};
|
||||
return withDiagnostics({ token, password });
|
||||
}
|
||||
|
||||
export { pickGatewaySelfPresence };
|
||||
|
||||
@@ -74,11 +74,8 @@ describe("installGatewayDaemonNonInteractive", () => {
|
||||
});
|
||||
|
||||
expect(resolveGatewayInstallToken).toHaveBeenCalledTimes(1);
|
||||
expect(buildGatewayInstallPlan).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
token: undefined,
|
||||
}),
|
||||
);
|
||||
expect(buildGatewayInstallPlan).toHaveBeenCalledTimes(1);
|
||||
expect("token" in buildGatewayInstallPlan.mock.calls[0][0]).toBe(false);
|
||||
expect(serviceInstall).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -55,7 +55,6 @@ export async function installGatewayDaemonNonInteractive(params: {
|
||||
const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({
|
||||
env: process.env,
|
||||
port,
|
||||
token: tokenResolution.token,
|
||||
runtime: daemonRuntimeRaw,
|
||||
warn: (message) => runtime.log(message),
|
||||
config: params.nextConfig,
|
||||
|
||||
Reference in New Issue
Block a user