fix(gateway): drop stale service env on reinstall

This commit is contained in:
Ayaan Zaidi
2026-04-21 13:07:56 +05:30
parent f14e91b39f
commit 6a4a60fe25
2 changed files with 64 additions and 1 deletions

View File

@@ -413,4 +413,38 @@ describe("runDaemonInstall", () => {
}
}
});
it("does not reuse stale service control env during forced reinstall", async () => {
service.isLoaded.mockResolvedValue(true);
service.readCommand.mockResolvedValue({
programArguments: ["openclaw", "gateway", "run"],
environment: {
OPENCLAW_STATE_DIR: "/tmp/openclaw-doctor-manual",
OPENCLAW_CONFIG_PATH: "/tmp/openclaw-doctor-manual/openclaw.json",
OPENCLAW_GATEWAY_TOKEN: "stale-service-token",
PATH: "/tmp/doctor-bin:/usr/bin",
NODE_OPTIONS: "--require /tmp/evil.js",
OPENAI_API_KEY: "service-openai-key",
},
} as never);
await runDaemonInstall({ json: true, force: true });
expect(buildGatewayInstallPlanMock).toHaveBeenCalledWith(
expect.objectContaining({
env: expect.objectContaining({
OPENAI_API_KEY: "service-openai-key",
}),
}),
);
const [firstArg] =
(buildGatewayInstallPlanMock.mock.calls.at(0) as [Record<string, unknown>] | undefined) ?? [];
const env = firstArg?.env as Record<string, string | undefined>;
expect(env.OPENCLAW_STATE_DIR).toBeUndefined();
expect(env.OPENCLAW_CONFIG_PATH).toBeUndefined();
expect(env.OPENCLAW_GATEWAY_TOKEN).toBeUndefined();
expect(env.NODE_OPTIONS).toBeUndefined();
expect(env.PATH).not.toContain("/tmp/doctor-bin");
expect(installDaemonServiceAndEmitMock).toHaveBeenCalledTimes(1);
});
});

View File

@@ -9,6 +9,11 @@ import { readConfigFileSnapshotForWrite } from "../../config/io.js";
import { resolveGatewayPort } from "../../config/paths.js";
import { resolveGatewayService } from "../../daemon/service.js";
import { isNonFatalSystemdInstallProbeError } from "../../daemon/systemd.js";
import {
isDangerousHostEnvOverrideVarName,
isDangerousHostEnvVarName,
normalizeEnvVarKey,
} from "../../infra/host-env-security.js";
import { defaultRuntime } from "../../runtime.js";
import { formatCliCommand } from "../command-format.js";
import { buildDaemonServiceSnapshot, installDaemonServiceAndEmit } from "./response.js";
@@ -26,8 +31,32 @@ function mergeInstallInvocationEnv(params: {
if (!params.existingServiceEnv || Object.keys(params.existingServiceEnv).length === 0) {
return params.env;
}
const preservedServiceEnv: NodeJS.ProcessEnv = {};
for (const [rawKey, rawValue] of Object.entries(params.existingServiceEnv)) {
const key = normalizeEnvVarKey(rawKey, { portable: true });
if (!key) {
continue;
}
const upper = key.toUpperCase();
if (
upper === "HOME" ||
upper === "PATH" ||
upper === "TMPDIR" ||
upper.startsWith("OPENCLAW_")
) {
continue;
}
if (isDangerousHostEnvVarName(key) || isDangerousHostEnvOverrideVarName(key)) {
continue;
}
const value = rawValue.trim();
if (!value) {
continue;
}
preservedServiceEnv[key] = value;
}
return {
...params.existingServiceEnv,
...preservedServiceEnv,
...params.env,
};
}