diff --git a/src/commands/daemon-install-helpers.ts b/src/commands/daemon-install-helpers.ts index a7d5ad727d7..aaa79979ebd 100644 --- a/src/commands/daemon-install-helpers.ts +++ b/src/commands/daemon-install-helpers.ts @@ -3,10 +3,7 @@ import os from "node:os"; import path from "node:path"; import type { AuthProfileStore } from "../agents/auth-profiles/types.js"; import { formatCliCommand } from "../cli/command-format.js"; -import { - collectDurableServiceEnvVars, - readStateDirDotEnvVars, -} from "../config/state-dir-dotenv.js"; +import { collectDurableServiceEnvVarSources } from "../config/state-dir-dotenv.js"; import type { OpenClawConfig } from "../config/types.js"; import { resolveSecretInputRef } from "../config/types.secrets.js"; import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js"; @@ -16,11 +13,16 @@ import { resolveGatewayProgramArguments, resolveOpenClawWrapperPath, } from "../daemon/program-args.js"; +import { + addServiceEnvPlanEntries, + compactServiceEnvPlanValueSources, + createMutableServiceEnvPlan, +} from "../daemon/service-env-plan.js"; +import { applyManagedServiceEnvRenderPolicy } from "../daemon/service-env-render-policy.js"; import { buildServiceEnvironment } from "../daemon/service-env.js"; import { formatManagedServiceEnvKeys, readManagedServiceEnvKeysFromEnvironment, - writeManagedServiceEnvKeysToEnvironment, } from "../daemon/service-managed-env.js"; import { isNonMinimalServicePathEntry } from "../daemon/service-path-policy.js"; import type { GatewayServiceEnvironmentValueSource } from "../daemon/service-types.js"; @@ -395,35 +397,6 @@ function resolveGatewayInstallWorkingDirectory(params: { return resolveGatewayStateDir(params.env); } -function retainLaunchAgentManagedServiceEnvValues(params: { - environment: Record; - durableEnvironment: Record; - managedServiceEnvKeys: string | undefined; - stateDirDotEnvEnvironment: Record; - serviceEnvironment: Record; - platform: NodeJS.Platform; -}): void { - if (params.platform !== "darwin" || !params.serviceEnvironment.OPENCLAW_LAUNCHD_LABEL?.trim()) { - return; - } - const managedKeys = readManagedServiceEnvKeysFromEnvironment({ - OPENCLAW_SERVICE_MANAGED_ENV_KEYS: params.managedServiceEnvKeys, - }); - if (managedKeys.size === 0) { - return; - } - for (const [rawKey, value] of Object.entries(params.stateDirDotEnvEnvironment)) { - const key = normalizeEnvVarKey(rawKey, { portable: true })?.toUpperCase(); - if (!key || !managedKeys.has(key) || typeof value !== "string" || !value.trim()) { - continue; - } - if (params.durableEnvironment[rawKey] !== value) { - continue; - } - params.environment[rawKey] = value; - } -} - async function buildGatewayInstallEnvironment(params: { env: Record; config?: OpenClawConfig; @@ -440,11 +413,11 @@ async function buildGatewayInstallEnvironment(params: { environment: Record; environmentValueSources: Record; }> { - const stateDirDotEnvEnvironment = readStateDirDotEnvVars(params.env); - const durableEnvironment = collectDurableServiceEnvVars({ - env: params.env, - config: params.config, - }); + const { stateDirDotEnvEnvironment, configEnvironment, durableEnvironment } = + collectDurableServiceEnvVarSources({ + env: params.env, + config: params.config, + }); const configSecretRefEnvironment = collectConfigSecretRefServiceEnvVars({ env: params.env, config: params.config, @@ -466,51 +439,33 @@ async function buildGatewayInstallEnvironment(params: { params.existingEnvironment, readManagedServiceEnvKeysFromEnvironment(params.existingEnvironment), ); - const environment: Record = { - ...preservedExistingEnvironment, - ...durableEnvironment, - ...configSecretRefEnvironment, - ...execSecretRefPassEnvEnvironment, - ...authProfileEnvironment, - }; - const environmentValueSources: Record = - {}; - for (const rawKey of Object.keys(preservedExistingEnvironment)) { - const normalizedKey = normalizeEnvVarKey(rawKey, { portable: true })?.toUpperCase(); - environmentValueSources[rawKey] = normalizedKey - ? (readExistingEnvironmentValueSource({ - existingEnvironmentValueSources: params.existingEnvironmentValueSources, - normalizedKey, - }) ?? "inline") - : "inline"; - } - for (const key of Object.keys({ - ...durableEnvironment, - ...configSecretRefEnvironment, - ...execSecretRefPassEnvEnvironment, - ...authProfileEnvironment, - })) { - environmentValueSources[key] = "inline"; - } + const plan = createMutableServiceEnvPlan(); + addServiceEnvPlanEntries(plan, preservedExistingEnvironment, { + source: "existing-preserved", + valueSource: ({ normalizedKey }) => + readExistingEnvironmentValueSource({ + existingEnvironmentValueSources: params.existingEnvironmentValueSources, + normalizedKey, + }) ?? "inline", + }); + addServiceEnvPlanEntries(plan, stateDirDotEnvEnvironment, { source: "state-dotenv" }); + addServiceEnvPlanEntries(plan, configEnvironment, { source: "config-env" }); + addServiceEnvPlanEntries(plan, configSecretRefEnvironment, { source: "config-secretref-env" }); + addServiceEnvPlanEntries(plan, execSecretRefPassEnvEnvironment, { source: "exec-passenv" }); + addServiceEnvPlanEntries(plan, authProfileEnvironment, { source: "auth-profile-env" }); const managedServiceEnvKeys = formatManagedServiceEnvKeys(durableEnvironment, { omitKeys: Object.keys(params.serviceEnvironment), }); - writeManagedServiceEnvKeysToEnvironment(environment, managedServiceEnvKeys); - retainLaunchAgentManagedServiceEnvValues({ - environment, - durableEnvironment, + applyManagedServiceEnvRenderPolicy({ + plan, managedServiceEnvKeys, - stateDirDotEnvEnvironment, serviceEnvironment: params.serviceEnvironment, platform: params.platform, }); - if (environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS) { - environmentValueSources.OPENCLAW_SERVICE_MANAGED_ENV_KEYS = "inline"; - } - Object.assign(environment, params.serviceEnvironment); - for (const key of Object.keys(params.serviceEnvironment)) { - environmentValueSources[key] = "inline"; - } + addServiceEnvPlanEntries(plan, params.serviceEnvironment, { + source: "service-generated", + includeRawKeys: true, + }); const mergedPath = mergeServicePath( params.serviceEnvironment.PATH, params.existingEnvironment?.PATH, @@ -518,15 +473,14 @@ async function buildGatewayInstallEnvironment(params: { params.platform, ); if (mergedPath) { - environment.PATH = mergedPath; - environmentValueSources.PATH = "inline"; + plan.environment.PATH = mergedPath; + plan.environmentValueSources.PATH = "inline"; } - for (const key of Object.keys(environmentValueSources)) { - if (!Object.hasOwn(environment, key)) { - delete environmentValueSources[key]; - } - } - return { environment, environmentValueSources }; + compactServiceEnvPlanValueSources(plan); + return { + environment: plan.environment, + environmentValueSources: plan.environmentValueSources, + }; } export async function buildGatewayInstallPlan(params: { diff --git a/src/config/state-dir-dotenv.ts b/src/config/state-dir-dotenv.ts index 76d360fe92b..196697fbbb4 100644 --- a/src/config/state-dir-dotenv.ts +++ b/src/config/state-dir-dotenv.ts @@ -54,6 +54,28 @@ export function readStateDirDotEnvVars( return readStateDirDotEnvVarsFromStateDir(stateDir); } +export type DurableServiceEnvVarSources = { + stateDirDotEnvEnvironment: Record; + configEnvironment: Record; + durableEnvironment: Record; +}; + +export function collectDurableServiceEnvVarSources(params: { + env: Record; + config?: OpenClawConfig; +}): DurableServiceEnvVarSources { + const stateDirDotEnvEnvironment = readStateDirDotEnvVars(params.env); + const configEnvironment = collectConfigServiceEnvVars(params.config); + return { + stateDirDotEnvEnvironment, + configEnvironment, + durableEnvironment: { + ...stateDirDotEnvEnvironment, + ...configEnvironment, + }, + }; +} + /** * Durable service env sources survive beyond the invoking shell and are safe to * persist into owner-only gateway service environment sources. @@ -66,8 +88,5 @@ export function collectDurableServiceEnvVars(params: { env: Record; config?: OpenClawConfig; }): Record { - return { - ...readStateDirDotEnvVars(params.env), - ...collectConfigServiceEnvVars(params.config), - }; + return collectDurableServiceEnvVarSources(params).durableEnvironment; } diff --git a/src/daemon/service-env-plan.ts b/src/daemon/service-env-plan.ts new file mode 100644 index 00000000000..647a01d2468 --- /dev/null +++ b/src/daemon/service-env-plan.ts @@ -0,0 +1,86 @@ +import { normalizeEnvVarKey } from "../infra/host-env-security.js"; +import type { GatewayServiceEnvironmentValueSource } from "./service-types.js"; + +export type ServiceEnvSource = + | "state-dotenv" + | "config-env" + | "config-secretref-env" + | "exec-passenv" + | "auth-profile-env" + | "existing-preserved" + | "service-generated"; + +export type ServiceEnvPlanEntry = { + rawKey: string; + normalizedKey: string; + value: string; + source: ServiceEnvSource; +}; + +export type MutableServiceEnvPlan = { + environment: Record; + environmentValueSources: Record; + entriesByNormalizedKey: Map; +}; + +export function createMutableServiceEnvPlan(): MutableServiceEnvPlan { + return { + environment: {}, + environmentValueSources: {}, + entriesByNormalizedKey: new Map(), + }; +} + +export function normalizeServiceEnvPlanKey(rawKey: string): string | undefined { + return normalizeEnvVarKey(rawKey, { portable: true })?.toUpperCase(); +} + +export function addServiceEnvPlanEntries( + plan: MutableServiceEnvPlan, + entries: Record, + options: { + source: ServiceEnvSource; + includeRawKeys?: boolean; + valueSource?: + | GatewayServiceEnvironmentValueSource + | ((params: { + rawKey: string; + normalizedKey: string; + }) => GatewayServiceEnvironmentValueSource | undefined); + }, +): void { + for (const [rawKey, rawValue] of Object.entries(entries)) { + if (typeof rawValue !== "string" || !rawValue.trim()) { + if (options.includeRawKeys) { + plan.environment[rawKey] = rawValue; + plan.environmentValueSources[rawKey] = "inline"; + } + continue; + } + const value = rawValue; + const normalizedKey = normalizeServiceEnvPlanKey(rawKey); + if (!normalizedKey) { + continue; + } + plan.environment[rawKey] = value; + const valueSource = + typeof options.valueSource === "function" + ? options.valueSource({ rawKey, normalizedKey }) + : options.valueSource; + plan.environmentValueSources[rawKey] = valueSource ?? "inline"; + plan.entriesByNormalizedKey.set(normalizedKey, { + rawKey, + normalizedKey, + value, + source: options.source, + }); + } +} + +export function compactServiceEnvPlanValueSources(plan: MutableServiceEnvPlan): void { + for (const key of Object.keys(plan.environmentValueSources)) { + if (!Object.hasOwn(plan.environment, key)) { + delete plan.environmentValueSources[key]; + } + } +} diff --git a/src/daemon/service-env-render-policy.ts b/src/daemon/service-env-render-policy.ts new file mode 100644 index 00000000000..c0d96e3dc22 --- /dev/null +++ b/src/daemon/service-env-render-policy.ts @@ -0,0 +1,43 @@ +import type { MutableServiceEnvPlan } from "./service-env-plan.js"; +import { + readManagedServiceEnvKeysFromEnvironment, + writeManagedServiceEnvKeysToEnvironment, +} from "./service-managed-env.js"; + +function isLaunchAgentServiceEnvironment(params: { + platform: NodeJS.Platform; + serviceEnvironment: Record; +}): boolean { + return ( + params.platform === "darwin" && + Boolean(params.serviceEnvironment.OPENCLAW_LAUNCHD_LABEL?.trim()) + ); +} + +export function applyManagedServiceEnvRenderPolicy(params: { + plan: MutableServiceEnvPlan; + managedServiceEnvKeys: string | undefined; + serviceEnvironment: Record; + platform: NodeJS.Platform; +}): void { + writeManagedServiceEnvKeysToEnvironment(params.plan.environment, params.managedServiceEnvKeys); + if (params.plan.environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS) { + params.plan.environmentValueSources.OPENCLAW_SERVICE_MANAGED_ENV_KEYS = "inline"; + } + if (!isLaunchAgentServiceEnvironment(params)) { + return; + } + const managedKeys = readManagedServiceEnvKeysFromEnvironment({ + OPENCLAW_SERVICE_MANAGED_ENV_KEYS: params.managedServiceEnvKeys, + }); + if (managedKeys.size === 0) { + return; + } + for (const entry of params.plan.entriesByNormalizedKey.values()) { + if (entry.source !== "state-dotenv" || !managedKeys.has(entry.normalizedKey)) { + continue; + } + params.plan.environment[entry.rawKey] = entry.value; + params.plan.environmentValueSources[entry.rawKey] = "inline"; + } +}