From bd156fa02e6c6817bdd97de0a687e898ec71d5ca Mon Sep 17 00:00:00 2001 From: stainlu Date: Tue, 5 May 2026 01:03:50 +0800 Subject: [PATCH] fix: preserve gateway install env sources --- CHANGELOG.md | 1 + src/cli/daemon-cli/install.test.ts | 43 ++++++++++++++++++++++++++++-- src/cli/daemon-cli/install.ts | 37 ++++++++++++++----------- 3 files changed, 64 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc60fa8aea1..1297184b6e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ Docs: https://docs.openclaw.ai - Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids while converting manifest catalog rows into emitted provider config, so `google/gemini-3.1-pro-preview` is used for testing instead of `google/gemini-3-pro-preview`. - Native apps: advertise the Gateway protocol compatibility range so chat and node sessions can connect to v3 gateways after additive v4 client updates. - Gateway/agents: keep stale `sessions_send` ACP manager and `web_fetch` runtime chunks importable after package updates, preventing live gateways from breaking before restart. Fixes #78804. Thanks @Gomesy72. +- Gateway/install: preserve service environment value-source metadata in `openclaw gateway install`, so systemd reinstall paths keep env-file-backed secrets out of inline unit metadata. Refs #77406. Thanks @brokemac79. - Gateway: avoid synchronous restart-sentinel state probes during post-attach startup, preventing slow Windows or redirected state directories from blocking channel turns. Fixes #79264. Thanks @liyi58. - Agents/auth: update successful model auth profile status with one locked store write, reducing post-model reply latency from duplicate `auth-profiles.json` saves. Thanks @mcaxtr. - Agents/image: honor explicit `image` tool model overrides even when `agents.defaults.imageModel` is unset, restoring one-off vision calls for configured multimodal providers. Fixes #79341. Thanks @haumanto. diff --git a/src/cli/daemon-cli/install.test.ts b/src/cli/daemon-cli/install.test.ts index b9da5238b9d..75062d47d4d 100644 --- a/src/cli/daemon-cli/install.test.ts +++ b/src/cli/daemon-cli/install.test.ts @@ -31,7 +31,15 @@ const resolveGatewayAuthMock = vi.hoisted(() => const resolveSecretRefValuesMock = vi.hoisted(() => vi.fn()); const randomTokenMock = vi.hoisted(() => vi.fn(() => "generated-token")); const createInstallPlanFixture = vi.hoisted(() => { - return async (params?: { wrapperPath?: string; env?: Record }) => { + return async (params?: { + wrapperPath?: string; + env?: Record; + }): Promise<{ + programArguments: string[]; + workingDirectory: string; + environment: Record; + environmentValueSources?: Record; + }> => { const environment: Record = {}; if (params?.wrapperPath || params?.env?.OPENCLAW_WRAPPER) { environment.OPENCLAW_WRAPPER = params.wrapperPath ?? params.env?.OPENCLAW_WRAPPER; @@ -48,7 +56,7 @@ const createInstallPlanFixture = vi.hoisted(() => { const buildGatewayInstallPlanMock = vi.hoisted(() => vi.fn(createInstallPlanFixture)); const parsePortMock = vi.hoisted(() => vi.fn(() => null)); const isGatewayDaemonRuntimeMock = vi.hoisted(() => vi.fn(() => true)); -const installDaemonServiceAndEmitMock = vi.hoisted(() => vi.fn(async () => {})); +const installDaemonServiceAndEmitMock = vi.hoisted(() => vi.fn(async (_params?: unknown) => {})); const actionState = vi.hoisted(() => ({ warnings: [] as string[], @@ -224,6 +232,7 @@ describe("runDaemonInstall", () => { installDaemonServiceAndEmitMock.mockReset(); service.isLoaded.mockReset(); service.stage.mockReset(); + service.install.mockReset(); service.readCommand.mockReset(); resetRuntimeCapture(); actionState.warnings.length = 0; @@ -254,6 +263,7 @@ describe("runDaemonInstall", () => { installDaemonServiceAndEmitMock.mockResolvedValue(undefined); service.isLoaded.mockResolvedValue(false); service.stage.mockResolvedValue(undefined); + service.install.mockResolvedValue(undefined); service.readCommand.mockResolvedValue(null); resolveNodeStartupTlsEnvironmentMock.mockReturnValue({ NODE_EXTRA_CA_CERTS: undefined, @@ -296,6 +306,35 @@ describe("runDaemonInstall", () => { ).toBe(true); }); + it("passes service environment value sources through to service install", async () => { + buildGatewayInstallPlanMock.mockResolvedValueOnce({ + programArguments: ["openclaw", "gateway", "run"], + workingDirectory: "/tmp", + environment: { + OPENROUTER_API_KEY: "or-operator-key", + }, + environmentValueSources: { + OPENROUTER_API_KEY: "file", + }, + }); + installDaemonServiceAndEmitMock.mockImplementationOnce(async (params?: unknown) => { + await (params as { install: () => Promise }).install(); + }); + + await runDaemonInstall({ json: true }); + + expect(service.install).toHaveBeenCalledWith( + expect.objectContaining({ + environment: { + OPENROUTER_API_KEY: "or-operator-key", + }, + environmentValueSources: { + OPENROUTER_API_KEY: "file", + }, + }), + ); + }); + it("does not treat env-template gateway.auth.token as plaintext during install", async () => { loadConfigMock.mockReturnValue({ gateway: { auth: { mode: "token", token: "${OPENCLAW_GATEWAY_TOKEN}" } }, diff --git a/src/cli/daemon-cli/install.ts b/src/cli/daemon-cli/install.ts index 8fb424b322c..7f7a8ce60d5 100644 --- a/src/cli/daemon-cli/install.ts +++ b/src/cli/daemon-cli/install.ts @@ -160,6 +160,7 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) { runtime: runtimeRaw, wrapperPath, existingEnvironment: existingServiceEnv, + existingEnvironmentValueSources: existingServiceCommand?.environmentValueSources, config: cfg, }); if (autoRefreshMessage) { @@ -207,21 +208,23 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) { } } - const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({ - env: installEnv, - port, - runtime: runtimeRaw, - wrapperPath, - existingEnvironment: existingServiceEnv, - warn: (message) => { - if (json) { - warnings.push(message); - } else { - defaultRuntime.log(message); - } - }, - config: cfg, - }); + const { programArguments, workingDirectory, environment, environmentValueSources } = + await buildGatewayInstallPlan({ + env: installEnv, + port, + runtime: runtimeRaw, + wrapperPath, + existingEnvironment: existingServiceEnv, + existingEnvironmentValueSources: existingServiceCommand?.environmentValueSources, + warn: (message) => { + if (json) { + warnings.push(message); + } else { + defaultRuntime.log(message); + } + }, + config: cfg, + }); await installDaemonServiceAndEmit({ serviceNoun: "Gateway", @@ -236,6 +239,7 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) { programArguments, workingDirectory, environment, + environmentValueSources, }); }, }); @@ -249,6 +253,7 @@ async function getGatewayServiceAutoRefreshMessage(params: { runtime: GatewayDaemonRuntime; wrapperPath?: string; existingEnvironment?: Record; + existingEnvironmentValueSources?: GatewayServiceCommandConfig["environmentValueSources"]; config: OpenClawConfig; }): Promise { try { @@ -264,6 +269,7 @@ async function getGatewayServiceAutoRefreshMessage(params: { runtime: params.runtime, wrapperPath: params.wrapperPath, existingEnvironment: params.existingEnvironment, + existingEnvironmentValueSources: params.existingEnvironmentValueSources, warn: () => undefined, config: params.config, }); @@ -284,6 +290,7 @@ async function getGatewayServiceAutoRefreshMessage(params: { runtime: params.runtime, wrapperPath: params.wrapperPath, existingEnvironment: params.existingEnvironment, + existingEnvironmentValueSources: params.existingEnvironmentValueSources, warn: () => undefined, config: params.config, });