refactor(daemon): centralize environment source lookup (#113283)

This commit is contained in:
Vincent Koc
2026-07-24 16:45:03 +08:00
committed by GitHub
parent e3c2a8be58
commit adf78cefbd
7 changed files with 54 additions and 73 deletions
+4 -4
View File
@@ -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",
},
});
+5 -23
View File
@@ -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<string, string | undefined> | 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, {});
+17
View File
@@ -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", () => {
+6 -13
View File
@@ -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);
+15 -5
View File
@@ -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<string, GatewayServiceEnvironmentValueSource | undefined>
| 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
+4 -4
View File
@@ -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",
},
});
+3 -24
View File
@@ -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<string, GatewayServiceEnvironmentValueSource | undefined>;
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<string, GatewayServiceEnvironmentValueSource | undefined>;
@@ -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;
}