refactor: source service env install planning

This commit is contained in:
Peter Steinberger
2026-05-04 01:46:57 +01:00
parent 53426cf611
commit 2b01bcf6c8
4 changed files with 191 additions and 89 deletions

View File

@@ -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<string, string | undefined>;
durableEnvironment: Record<string, string | undefined>;
managedServiceEnvKeys: string | undefined;
stateDirDotEnvEnvironment: Record<string, string | undefined>;
serviceEnvironment: Record<string, string | undefined>;
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<string, string | undefined>;
config?: OpenClawConfig;
@@ -440,11 +413,11 @@ async function buildGatewayInstallEnvironment(params: {
environment: Record<string, string | undefined>;
environmentValueSources: Record<string, GatewayServiceEnvironmentValueSource | undefined>;
}> {
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<string, string | undefined> = {
...preservedExistingEnvironment,
...durableEnvironment,
...configSecretRefEnvironment,
...execSecretRefPassEnvEnvironment,
...authProfileEnvironment,
};
const environmentValueSources: Record<string, GatewayServiceEnvironmentValueSource | undefined> =
{};
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: {

View File

@@ -54,6 +54,28 @@ export function readStateDirDotEnvVars(
return readStateDirDotEnvVarsFromStateDir(stateDir);
}
export type DurableServiceEnvVarSources = {
stateDirDotEnvEnvironment: Record<string, string>;
configEnvironment: Record<string, string>;
durableEnvironment: Record<string, string>;
};
export function collectDurableServiceEnvVarSources(params: {
env: Record<string, string | undefined>;
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<string, string | undefined>;
config?: OpenClawConfig;
}): Record<string, string> {
return {
...readStateDirDotEnvVars(params.env),
...collectConfigServiceEnvVars(params.config),
};
return collectDurableServiceEnvVarSources(params).durableEnvironment;
}

View File

@@ -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<string, string | undefined>;
environmentValueSources: Record<string, GatewayServiceEnvironmentValueSource | undefined>;
entriesByNormalizedKey: Map<string, ServiceEnvPlanEntry>;
};
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<string, string | undefined>,
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];
}
}
}

View File

@@ -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<string, string | undefined>;
}): boolean {
return (
params.platform === "darwin" &&
Boolean(params.serviceEnvironment.OPENCLAW_LAUNCHD_LABEL?.trim())
);
}
export function applyManagedServiceEnvRenderPolicy(params: {
plan: MutableServiceEnvPlan;
managedServiceEnvKeys: string | undefined;
serviceEnvironment: Record<string, string | undefined>;
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";
}
}