fix(daemon): ignore recursive Windows gateway wrapper

Fixes #86007.

Release note: Windows gateway install/update now ignores a persisted OPENCLAW_WRAPPER when it points back at the generated gateway.cmd task script, preventing recursive gateway startup while keeping valid wrapper installs intact.

Credit: thanks @luoyanglang for the fix and proof.
This commit is contained in:
狼哥
2026-05-27 06:42:25 +08:00
committed by GitHub
parent eb15c443fc
commit 126a3363a3
4 changed files with 99 additions and 18 deletions
@@ -275,6 +275,42 @@ describe("buildGatewayInstallPlan", () => {
expect(plan.environment.OPENCLAW_WRAPPER).toBe(wrapperPath);
});
it("clears a Windows wrapper env that points at the generated gateway.cmd script", async () => {
const selfWrapperPath = path.join(isolatedHome, ".openclaw", "gateway.cmd");
const warn = vi.fn();
mockNodeGatewayPlanFixture({
serviceEnvironment: {
OPENCLAW_PORT: "3000",
},
});
const plan = await buildGatewayInstallPlan({
env: isolatedPlanEnv({
OPENCLAW_WRAPPER: selfWrapperPath,
}),
port: 3000,
runtime: "node",
platform: "win32",
warn,
});
expect(mocks.resolveGatewayProgramArguments).toHaveBeenCalledOnce();
expect(
firstMockArg(mocks.resolveGatewayProgramArguments, "resolveGatewayProgramArguments")
.wrapperPath,
).toBeUndefined();
expect(mocks.buildServiceEnvironment).toHaveBeenCalledOnce();
expect(
firstMockArg(mocks.buildServiceEnvironment, "buildServiceEnvironment").env?.OPENCLAW_WRAPPER,
).toBeUndefined();
expect(plan.environment.OPENCLAW_WRAPPER).toBeUndefined();
expect(warn).toHaveBeenCalledWith(
expect.stringContaining(
"Ignoring OPENCLAW_WRAPPER because it points to the Windows task script",
),
);
});
it("tracks safe config env keys without embedding literal values", async () => {
mockNodeGatewayPlanFixture({
serviceEnvironment: {
+47 -5
View File
@@ -7,7 +7,7 @@ import { collectDurableServiceEnvVarSources } from "../config/state-dir-dotenv.j
import type { OpenClawConfig } from "../config/types.js";
import { resolveSecretInputRef } from "../config/types.secrets.js";
import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js";
import { resolveGatewayStateDir } from "../daemon/paths.js";
import { resolveGatewayStateDir, resolveGatewayTaskScriptPath } from "../daemon/paths.js";
import {
OPENCLAW_WRAPPER_ENV_KEY,
resolveGatewayProgramArguments,
@@ -519,12 +519,24 @@ export async function buildGatewayInstallPlan(params: {
devMode: params.devMode,
nodePath: params.nodePath,
});
const wrapperPath = await resolveOpenClawWrapperPath(
params.wrapperPath ?? params.env[OPENCLAW_WRAPPER_ENV_KEY],
);
const wrapperInput = params.wrapperPath ?? params.env[OPENCLAW_WRAPPER_ENV_KEY];
const wrapperPointsAtWindowsTaskScript =
Boolean(wrapperInput?.trim()) &&
platform === "win32" &&
isSameServicePath(wrapperInput, resolveGatewayTaskScriptPath(params.env), platform);
if (wrapperPointsAtWindowsTaskScript) {
params.warn?.(
`Ignoring ${OPENCLAW_WRAPPER_ENV_KEY} because it points to the Windows task script; using the OpenClaw gateway entrypoint directly to avoid a recursive gateway.cmd wrapper.`,
);
}
const wrapperPath = wrapperPointsAtWindowsTaskScript
? undefined
: await resolveOpenClawWrapperPath(wrapperInput);
const serviceInputEnv: Record<string, string | undefined> = wrapperPath
? { ...params.env, [OPENCLAW_WRAPPER_ENV_KEY]: wrapperPath }
: params.env;
: wrapperPointsAtWindowsTaskScript
? omitEnvKey(params.env, OPENCLAW_WRAPPER_ENV_KEY)
: params.env;
const { programArguments, workingDirectory } = await resolveGatewayProgramArguments({
port: params.port,
dev: devMode,
@@ -578,6 +590,36 @@ export async function buildGatewayInstallPlan(params: {
};
}
function normalizeServicePathForCompare(
value: string | undefined,
platform: NodeJS.Platform,
): string | undefined {
const trimmed = value?.trim();
if (!trimmed) {
return undefined;
}
return platform === "win32" ? path.win32.resolve(trimmed).toLowerCase() : path.resolve(trimmed);
}
function isSameServicePath(
left: string | undefined,
right: string | undefined,
platform: NodeJS.Platform,
): boolean {
const normalizedLeft = normalizeServicePathForCompare(left, platform);
const normalizedRight = normalizeServicePathForCompare(right, platform);
return Boolean(normalizedLeft && normalizedRight && normalizedLeft === normalizedRight);
}
function omitEnvKey(
env: Record<string, string | undefined>,
key: string,
): Record<string, string | undefined> {
const next = { ...env };
delete next[key];
return next;
}
export function gatewayInstallErrorHint(platform = process.platform): string {
return platform === "win32"
? "Tip: native Windows now falls back to a per-user Startup-folder login item when Scheduled Task creation is denied; if install still fails, rerun from an elevated PowerShell or skip service install."
+14
View File
@@ -41,3 +41,17 @@ export function resolveGatewayStateDir(env: Record<string, string | undefined>):
const suffix = resolveGatewayProfileSuffix(env.OPENCLAW_PROFILE);
return path.join(home, `.openclaw${suffix}`);
}
export function resolveGatewayTaskScriptPath(env: Record<string, string | undefined>): string {
const override = normalizeOptionalString(env.OPENCLAW_TASK_SCRIPT);
if (override) {
return override;
}
const scriptName = normalizeOptionalString(env.OPENCLAW_TASK_SCRIPT_NAME) || "gateway.cmd";
if (/[/\\]|\.\./.test(scriptName)) {
throw new Error(
`OPENCLAW_TASK_SCRIPT_NAME must be a file name only, not a path: ${scriptName}`,
);
}
return path.join(resolveGatewayStateDir(env), scriptName);
}
+2 -13
View File
@@ -13,7 +13,7 @@ import { parseCmdScriptCommandLine, quoteCmdScriptArg } from "./cmd-argv.js";
import { assertNoCmdLineBreak, parseCmdSetAssignment, renderCmdSetAssignment } from "./cmd-set.js";
import { resolveGatewayServiceDescription, resolveGatewayWindowsTaskName } from "./constants.js";
import { formatLine, writeFormattedLines } from "./output.js";
import { resolveGatewayStateDir } from "./paths.js";
import { resolveGatewayTaskScriptPath } from "./paths.js";
import { parseKeyValueOutput } from "./runtime-parse.js";
import { execSchtasks } from "./schtasks-exec.js";
import type { GatewayServiceRuntime } from "./service-runtime.js";
@@ -47,18 +47,7 @@ function shouldFallbackToStartupEntry(params: { code: number; detail: string }):
}
export function resolveTaskScriptPath(env: GatewayServiceEnv): string {
const override = env.OPENCLAW_TASK_SCRIPT?.trim();
if (override) {
return override;
}
const scriptName = env.OPENCLAW_TASK_SCRIPT_NAME?.trim() || "gateway.cmd";
if (/[/\\]|\.\./.test(scriptName)) {
throw new Error(
`OPENCLAW_TASK_SCRIPT_NAME must be a file name only, not a path: ${scriptName}`,
);
}
const stateDir = resolveGatewayStateDir(env);
return path.join(stateDir, scriptName);
return resolveGatewayTaskScriptPath(env);
}
function resolveWindowsStartupDir(env: GatewayServiceEnv): string {