From adf78cefbd73b280bfe935f2aa18417d2186bd48 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 24 Jul 2026 16:45:03 +0800 Subject: [PATCH] refactor(daemon): centralize environment source lookup (#113283) --- src/commands/daemon-install-helpers.test.ts | 8 +++--- src/commands/daemon-install-helpers.ts | 28 ++++----------------- src/daemon/service-audit.test.ts | 17 +++++++++++++ src/daemon/service-audit.ts | 19 +++++--------- src/daemon/service-managed-env.ts | 20 +++++++++++---- src/daemon/systemd.test.ts | 8 +++--- src/daemon/systemd.ts | 27 +++----------------- 7 files changed, 54 insertions(+), 73 deletions(-) diff --git a/src/commands/daemon-install-helpers.test.ts b/src/commands/daemon-install-helpers.test.ts index 70efcb4a9159..9754bd2dcaa5 100644 --- a/src/commands/daemon-install-helpers.test.ts +++ b/src/commands/daemon-install-helpers.test.ts @@ -1705,7 +1705,7 @@ describe("buildGatewayInstallPlan — dotenv merge", () => { expect(plan.environment.CUSTOM_TOOL_HOME).toBe("/Users/test/.custom-tool"); }); - it("keeps source metadata for EnvironmentFile-backed preserved vars", async () => { + it("keeps differently-cased source metadata for EnvironmentFile-backed preserved vars", async () => { mockNodeGatewayPlanFixture({ serviceEnvironment: { HOME: "/from-service", @@ -1723,9 +1723,9 @@ describe("buildGatewayInstallPlan — dotenv merge", () => { OPENCLAW_GATEWAY_TOKEN: "old-token", }, existingEnvironmentValueSources: { - OPENROUTER_API_KEY: "file", - CUSTOM_TOOL_HOME: "inline", - OPENCLAW_GATEWAY_TOKEN: "file", + openrouter_api_key: "file", + custom_tool_home: "inline", + openclaw_gateway_token: "file", }, }); diff --git a/src/commands/daemon-install-helpers.ts b/src/commands/daemon-install-helpers.ts index 2f9dc8320822..5994a06f286f 100644 --- a/src/commands/daemon-install-helpers.ts +++ b/src/commands/daemon-install-helpers.ts @@ -24,6 +24,7 @@ import { buildServiceEnvironment } from "../daemon/service-env.js"; import { formatManagedServiceEnvKeys, hasEnvironmentFileSource, + readEnvironmentValueSource, readManagedServiceEnvKeysFromEnvironment, } from "../daemon/service-managed-env.js"; import { isNonMinimalServicePathEntry } from "../daemon/service-path-policy.js"; @@ -500,22 +501,6 @@ function collectPreservedExistingServiceEnvVars( return preserved; } -function readExistingEnvironmentValueSource(params: { - existingEnvironmentValueSources?: Record< - string, - GatewayServiceEnvironmentValueSource | undefined - >; - normalizedKey: string; -}): GatewayServiceEnvironmentValueSource | undefined { - for (const [rawKey, source] of Object.entries(params.existingEnvironmentValueSources ?? {})) { - const key = normalizeEnvVarKey(rawKey, { portable: true })?.toUpperCase(); - if (key === params.normalizedKey) { - return source; - } - } - return undefined; -} - function collectExistingEnvironmentFileManagedServiceEnvVars(params: { existingEnvironment: Record | undefined; existingEnvironmentValueSources?: Record< @@ -540,10 +525,10 @@ function collectExistingEnvironmentFileManagedServiceEnvVars(params: { if (isDangerousHostEnvVarName(key) || isDangerousHostEnvOverrideVarName(key)) { continue; } - const source = readExistingEnvironmentValueSource({ - existingEnvironmentValueSources: params.existingEnvironmentValueSources, + const source = readEnvironmentValueSource( + params.existingEnvironmentValueSources, normalizedKey, - }); + ); if (!hasEnvironmentFileSource(source)) { continue; } @@ -651,10 +636,7 @@ async function buildGatewayInstallEnvironment(params: { const plan = createMutableServiceEnvPlan(); addServiceEnvPlanEntries(plan, preservedExistingEnvironment, { valueSource: ({ normalizedKey }) => - readExistingEnvironmentValueSource({ - existingEnvironmentValueSources: params.existingEnvironmentValueSources, - normalizedKey, - }) ?? "inline", + readEnvironmentValueSource(params.existingEnvironmentValueSources, normalizedKey) ?? "inline", }); addServiceEnvPlanEntries(plan, stateDirDotEnvEnvironment, {}); addServiceEnvPlanEntries(plan, configEnvironment, {}); diff --git a/src/daemon/service-audit.test.ts b/src/daemon/service-audit.test.ts index 0b1f607c8d43..662c5cbf9865 100644 --- a/src/daemon/service-audit.test.ts +++ b/src/daemon/service-audit.test.ts @@ -715,6 +715,23 @@ describe("auditGatewayServiceConfig", () => { expect(hasIssue(audit, SERVICE_AUDIT_CODES.gatewayProxyEnvEmbedded)).toBe(true); }); + + it("matches managed and proxy source metadata keys case-insensitively", async () => { + const audit = await createGatewayAudit({ + expectedManagedServiceEnvKeys: ["TAVILY_API_KEY"], + extraEnvironment: { + TAVILY_API_KEY: "tvly-test", + HTTPS_PROXY: "https://proxy.local:7890", + }, + environmentValueSources: { + tavily_api_key: "file", + https_proxy: "file", + }, + }); + + expect(hasIssue(audit, SERVICE_AUDIT_CODES.gatewayManagedEnvEmbedded)).toBe(false); + expect(hasIssue(audit, SERVICE_AUDIT_CODES.gatewayProxyEnvEmbedded)).toBe(false); + }); }); describe("checkTokenDrift", () => { diff --git a/src/daemon/service-audit.ts b/src/daemon/service-audit.ts index 6c676eadf2bf..16f9487ce066 100644 --- a/src/daemon/service-audit.ts +++ b/src/daemon/service-audit.ts @@ -27,6 +27,7 @@ import { collectInlineManagedServiceEnvKeys, hasInlineEnvironmentSource, isEnvironmentFileOnlySource, + readEnvironmentValueSource, } from "./service-managed-env.js"; import { isNonMinimalServicePathEntry, normalizeServicePathEntry } from "./service-path-policy.js"; import type { GatewayServiceEnvironmentValueSource } from "./service-types.js"; @@ -393,18 +394,6 @@ function normalizeServiceEnvKey(key: string): string | null { return normalizeEnvVarKey(key, { portable: true })?.toUpperCase() ?? null; } -function readEnvironmentValueSource( - command: GatewayServiceCommand, - normalizedKey: string, -): GatewayServiceEnvironmentValueSource | undefined { - for (const [rawKey, source] of Object.entries(command?.environmentValueSources ?? {})) { - if (normalizeServiceEnvKey(rawKey) === normalizedKey) { - return source; - } - } - return undefined; -} - const SERVICE_PROXY_ENV_KEY_SET = new Set( SERVICE_PROXY_ENV_KEYS.flatMap((key) => { const normalized = normalizeServiceEnvKey(key); @@ -425,7 +414,11 @@ function collectInlineProxyEnvKeys(command: GatewayServiceCommand): string[] { if (!normalized || !SERVICE_PROXY_ENV_KEY_SET.has(normalized)) { continue; } - if (!hasInlineEnvironmentSource(readEnvironmentValueSource(command, normalized))) { + if ( + !hasInlineEnvironmentSource( + readEnvironmentValueSource(command.environmentValueSources, normalized), + ) + ) { continue; } inlineKeys.push(normalized); diff --git a/src/daemon/service-managed-env.ts b/src/daemon/service-managed-env.ts index 3fbe6540eceb..63ec828e02b2 100644 --- a/src/daemon/service-managed-env.ts +++ b/src/daemon/service-managed-env.ts @@ -114,11 +114,17 @@ export function writeManagedServiceEnvKeysToEnvironment( environment[MANAGED_SERVICE_ENV_KEYS_VAR] = value; } -function readEnvironmentValueSource( - command: ServiceEnvCommand, - normalizedKey: string, +export function readEnvironmentValueSource( + environmentValueSources: + | Record + | undefined, + key: string, ): GatewayServiceEnvironmentValueSource | undefined { - for (const [rawKey, source] of Object.entries(command?.environmentValueSources ?? {})) { + const normalizedKey = normalizeServiceEnvKey(key); + if (!normalizedKey) { + return undefined; + } + for (const [rawKey, source] of Object.entries(environmentValueSources ?? {})) { if (normalizeServiceEnvKey(rawKey) === normalizedKey) { return source; } @@ -155,7 +161,11 @@ export function collectInlineManagedServiceEnvKeys( if (normalized === MANAGED_SERVICE_ENV_KEYS_VAR) { continue; } - if (!hasInlineEnvironmentSource(readEnvironmentValueSource(command, normalized))) { + if ( + !hasInlineEnvironmentSource( + readEnvironmentValueSource(command.environmentValueSources, normalized), + ) + ) { continue; } // Only inline/file-overlap sources can be repaired from the service command diff --git a/src/daemon/systemd.test.ts b/src/daemon/systemd.test.ts index 8b99f17f1322..cd690941ae18 100644 --- a/src/daemon/systemd.test.ts +++ b/src/daemon/systemd.test.ts @@ -1322,7 +1322,7 @@ describe("stageSystemdService", () => { }); }); - it("writes node file-backed managed values to the node env file instead of the unit", async () => { + it("matches differently-cased source metadata when writing node file-backed values", async () => { await withStageFixture(async ({ env, stateDir, unitPath, envFilePath, nodeEnvFilePath }) => { await fs.rm(stateDir, { recursive: true, force: true }); const gatewayPassword = 'symbol " \\ $ `'; // pragma: allowlist secret @@ -1342,9 +1342,9 @@ describe("stageSystemdService", () => { OPENCLAW_SERVICE_KIND: "node", }, environmentValueSources: { - OPENCLAW_GATEWAY_TOKEN: "file", - OPENCLAW_GATEWAY_PASSWORD: "file", // pragma: allowlist secret - OPENCLAW_SERVICE_MANAGED_ENV_KEYS: "inline", + openclaw_gateway_token: "file", + openclaw_gateway_password: "file", // pragma: allowlist secret + openclaw_service_managed_env_keys: "inline", }, }); diff --git a/src/daemon/systemd.ts b/src/daemon/systemd.ts index e08d96a931fd..e755f8b28e3b 100644 --- a/src/daemon/systemd.ts +++ b/src/daemon/systemd.ts @@ -31,6 +31,7 @@ import { hasEnvironmentFileSource, hasInlineEnvironmentSource, isEnvironmentFileOnlySource, + readEnvironmentValueSource, readManagedServiceEnvKeysFromEnvironment, } from "./service-managed-env.js"; import { createGatewayLifecycleMutationReporter } from "./service-mutation.js"; @@ -254,22 +255,6 @@ function normalizeSystemdEnvironmentKey(key: string): string | null { return normalizeEnvVarKey(key, { portable: true })?.toUpperCase() ?? null; } -function readSystemdEnvironmentValueSource(params: { - environmentValueSources?: Record; - key: string; -}): GatewayServiceEnvironmentValueSource | undefined { - const normalizedKey = normalizeSystemdEnvironmentKey(params.key); - if (!normalizedKey) { - return undefined; - } - for (const [rawKey, source] of Object.entries(params.environmentValueSources ?? {})) { - if (normalizeSystemdEnvironmentKey(rawKey) === normalizedKey) { - return source; - } - } - return undefined; -} - function collectSystemdInlineManagedKeys(params: { environment?: GatewayServiceEnv; environmentValueSources?: Record; @@ -288,10 +273,7 @@ function collectSystemdInlineManagedKeys(params: { if (!key) { continue; } - const source = readSystemdEnvironmentValueSource({ - environmentValueSources: params.environmentValueSources, - key: rawKey, - }); + const source = readEnvironmentValueSource(params.environmentValueSources, rawKey); if (hasInlineEnvironmentSource(source) && !hasEnvironmentFileSource(source)) { keys.add(key); } @@ -1040,10 +1022,7 @@ async function writeSystemdUnit({ if (typeof value !== "string") { return false; } - const source = readSystemdEnvironmentValueSource({ - environmentValueSources, - key, - }); + const source = readEnvironmentValueSource(environmentValueSources, key); if (hasEnvironmentFileSource(source) && isUnresolvedShellReference(value)) { return false; }