Skip to content

Commit 2b01bcf

Browse files
committed
refactor: source service env install planning
1 parent 53426cf commit 2b01bcf

4 files changed

Lines changed: 191 additions & 89 deletions

File tree

src/commands/daemon-install-helpers.ts

Lines changed: 39 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ import os from "node:os";
33
import path from "node:path";
44
import type { AuthProfileStore } from "../agents/auth-profiles/types.js";
55
import { formatCliCommand } from "../cli/command-format.js";
6-
import {
7-
collectDurableServiceEnvVars,
8-
readStateDirDotEnvVars,
9-
} from "../config/state-dir-dotenv.js";
6+
import { collectDurableServiceEnvVarSources } from "../config/state-dir-dotenv.js";
107
import type { OpenClawConfig } from "../config/types.js";
118
import { resolveSecretInputRef } from "../config/types.secrets.js";
129
import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js";
@@ -16,11 +13,16 @@ import {
1613
resolveGatewayProgramArguments,
1714
resolveOpenClawWrapperPath,
1815
} from "../daemon/program-args.js";
16+
import {
17+
addServiceEnvPlanEntries,
18+
compactServiceEnvPlanValueSources,
19+
createMutableServiceEnvPlan,
20+
} from "../daemon/service-env-plan.js";
21+
import { applyManagedServiceEnvRenderPolicy } from "../daemon/service-env-render-policy.js";
1922
import { buildServiceEnvironment } from "../daemon/service-env.js";
2023
import {
2124
formatManagedServiceEnvKeys,
2225
readManagedServiceEnvKeysFromEnvironment,
23-
writeManagedServiceEnvKeysToEnvironment,
2426
} from "../daemon/service-managed-env.js";
2527
import { isNonMinimalServicePathEntry } from "../daemon/service-path-policy.js";
2628
import type { GatewayServiceEnvironmentValueSource } from "../daemon/service-types.js";
@@ -395,35 +397,6 @@ function resolveGatewayInstallWorkingDirectory(params: {
395397
return resolveGatewayStateDir(params.env);
396398
}
397399

398-
function retainLaunchAgentManagedServiceEnvValues(params: {
399-
environment: Record<string, string | undefined>;
400-
durableEnvironment: Record<string, string | undefined>;
401-
managedServiceEnvKeys: string | undefined;
402-
stateDirDotEnvEnvironment: Record<string, string | undefined>;
403-
serviceEnvironment: Record<string, string | undefined>;
404-
platform: NodeJS.Platform;
405-
}): void {
406-
if (params.platform !== "darwin" || !params.serviceEnvironment.OPENCLAW_LAUNCHD_LABEL?.trim()) {
407-
return;
408-
}
409-
const managedKeys = readManagedServiceEnvKeysFromEnvironment({
410-
OPENCLAW_SERVICE_MANAGED_ENV_KEYS: params.managedServiceEnvKeys,
411-
});
412-
if (managedKeys.size === 0) {
413-
return;
414-
}
415-
for (const [rawKey, value] of Object.entries(params.stateDirDotEnvEnvironment)) {
416-
const key = normalizeEnvVarKey(rawKey, { portable: true })?.toUpperCase();
417-
if (!key || !managedKeys.has(key) || typeof value !== "string" || !value.trim()) {
418-
continue;
419-
}
420-
if (params.durableEnvironment[rawKey] !== value) {
421-
continue;
422-
}
423-
params.environment[rawKey] = value;
424-
}
425-
}
426-
427400
async function buildGatewayInstallEnvironment(params: {
428401
env: Record<string, string | undefined>;
429402
config?: OpenClawConfig;
@@ -440,11 +413,11 @@ async function buildGatewayInstallEnvironment(params: {
440413
environment: Record<string, string | undefined>;
441414
environmentValueSources: Record<string, GatewayServiceEnvironmentValueSource | undefined>;
442415
}> {
443-
const stateDirDotEnvEnvironment = readStateDirDotEnvVars(params.env);
444-
const durableEnvironment = collectDurableServiceEnvVars({
445-
env: params.env,
446-
config: params.config,
447-
});
416+
const { stateDirDotEnvEnvironment, configEnvironment, durableEnvironment } =
417+
collectDurableServiceEnvVarSources({
418+
env: params.env,
419+
config: params.config,
420+
});
448421
const configSecretRefEnvironment = collectConfigSecretRefServiceEnvVars({
449422
env: params.env,
450423
config: params.config,
@@ -466,67 +439,48 @@ async function buildGatewayInstallEnvironment(params: {
466439
params.existingEnvironment,
467440
readManagedServiceEnvKeysFromEnvironment(params.existingEnvironment),
468441
);
469-
const environment: Record<string, string | undefined> = {
470-
...preservedExistingEnvironment,
471-
...durableEnvironment,
472-
...configSecretRefEnvironment,
473-
...execSecretRefPassEnvEnvironment,
474-
...authProfileEnvironment,
475-
};
476-
const environmentValueSources: Record<string, GatewayServiceEnvironmentValueSource | undefined> =
477-
{};
478-
for (const rawKey of Object.keys(preservedExistingEnvironment)) {
479-
const normalizedKey = normalizeEnvVarKey(rawKey, { portable: true })?.toUpperCase();
480-
environmentValueSources[rawKey] = normalizedKey
481-
? (readExistingEnvironmentValueSource({
482-
existingEnvironmentValueSources: params.existingEnvironmentValueSources,
483-
normalizedKey,
484-
}) ?? "inline")
485-
: "inline";
486-
}
487-
for (const key of Object.keys({
488-
...durableEnvironment,
489-
...configSecretRefEnvironment,
490-
...execSecretRefPassEnvEnvironment,
491-
...authProfileEnvironment,
492-
})) {
493-
environmentValueSources[key] = "inline";
494-
}
442+
const plan = createMutableServiceEnvPlan();
443+
addServiceEnvPlanEntries(plan, preservedExistingEnvironment, {
444+
source: "existing-preserved",
445+
valueSource: ({ normalizedKey }) =>
446+
readExistingEnvironmentValueSource({
447+
existingEnvironmentValueSources: params.existingEnvironmentValueSources,
448+
normalizedKey,
449+
}) ?? "inline",
450+
});
451+
addServiceEnvPlanEntries(plan, stateDirDotEnvEnvironment, { source: "state-dotenv" });
452+
addServiceEnvPlanEntries(plan, configEnvironment, { source: "config-env" });
453+
addServiceEnvPlanEntries(plan, configSecretRefEnvironment, { source: "config-secretref-env" });
454+
addServiceEnvPlanEntries(plan, execSecretRefPassEnvEnvironment, { source: "exec-passenv" });
455+
addServiceEnvPlanEntries(plan, authProfileEnvironment, { source: "auth-profile-env" });
495456
const managedServiceEnvKeys = formatManagedServiceEnvKeys(durableEnvironment, {
496457
omitKeys: Object.keys(params.serviceEnvironment),
497458
});
498-
writeManagedServiceEnvKeysToEnvironment(environment, managedServiceEnvKeys);
499-
retainLaunchAgentManagedServiceEnvValues({
500-
environment,
501-
durableEnvironment,
459+
applyManagedServiceEnvRenderPolicy({
460+
plan,
502461
managedServiceEnvKeys,
503-
stateDirDotEnvEnvironment,
504462
serviceEnvironment: params.serviceEnvironment,
505463
platform: params.platform,
506464
});
507-
if (environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS) {
508-
environmentValueSources.OPENCLAW_SERVICE_MANAGED_ENV_KEYS = "inline";
509-
}
510-
Object.assign(environment, params.serviceEnvironment);
511-
for (const key of Object.keys(params.serviceEnvironment)) {
512-
environmentValueSources[key] = "inline";
513-
}
465+
addServiceEnvPlanEntries(plan, params.serviceEnvironment, {
466+
source: "service-generated",
467+
includeRawKeys: true,
468+
});
514469
const mergedPath = mergeServicePath(
515470
params.serviceEnvironment.PATH,
516471
params.existingEnvironment?.PATH,
517472
params.serviceEnvironment.TMPDIR,
518473
params.platform,
519474
);
520475
if (mergedPath) {
521-
environment.PATH = mergedPath;
522-
environmentValueSources.PATH = "inline";
476+
plan.environment.PATH = mergedPath;
477+
plan.environmentValueSources.PATH = "inline";
523478
}
524-
for (const key of Object.keys(environmentValueSources)) {
525-
if (!Object.hasOwn(environment, key)) {
526-
delete environmentValueSources[key];
527-
}
528-
}
529-
return { environment, environmentValueSources };
479+
compactServiceEnvPlanValueSources(plan);
480+
return {
481+
environment: plan.environment,
482+
environmentValueSources: plan.environmentValueSources,
483+
};
530484
}
531485

532486
export async function buildGatewayInstallPlan(params: {

src/config/state-dir-dotenv.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,28 @@ export function readStateDirDotEnvVars(
5454
return readStateDirDotEnvVarsFromStateDir(stateDir);
5555
}
5656

57+
export type DurableServiceEnvVarSources = {
58+
stateDirDotEnvEnvironment: Record<string, string>;
59+
configEnvironment: Record<string, string>;
60+
durableEnvironment: Record<string, string>;
61+
};
62+
63+
export function collectDurableServiceEnvVarSources(params: {
64+
env: Record<string, string | undefined>;
65+
config?: OpenClawConfig;
66+
}): DurableServiceEnvVarSources {
67+
const stateDirDotEnvEnvironment = readStateDirDotEnvVars(params.env);
68+
const configEnvironment = collectConfigServiceEnvVars(params.config);
69+
return {
70+
stateDirDotEnvEnvironment,
71+
configEnvironment,
72+
durableEnvironment: {
73+
...stateDirDotEnvEnvironment,
74+
...configEnvironment,
75+
},
76+
};
77+
}
78+
5779
/**
5880
* Durable service env sources survive beyond the invoking shell and are safe to
5981
* persist into owner-only gateway service environment sources.
@@ -66,8 +88,5 @@ export function collectDurableServiceEnvVars(params: {
6688
env: Record<string, string | undefined>;
6789
config?: OpenClawConfig;
6890
}): Record<string, string> {
69-
return {
70-
...readStateDirDotEnvVars(params.env),
71-
...collectConfigServiceEnvVars(params.config),
72-
};
91+
return collectDurableServiceEnvVarSources(params).durableEnvironment;
7392
}

src/daemon/service-env-plan.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { normalizeEnvVarKey } from "../infra/host-env-security.js";
2+
import type { GatewayServiceEnvironmentValueSource } from "./service-types.js";
3+
4+
export type ServiceEnvSource =
5+
| "state-dotenv"
6+
| "config-env"
7+
| "config-secretref-env"
8+
| "exec-passenv"
9+
| "auth-profile-env"
10+
| "existing-preserved"
11+
| "service-generated";
12+
13+
export type ServiceEnvPlanEntry = {
14+
rawKey: string;
15+
normalizedKey: string;
16+
value: string;
17+
source: ServiceEnvSource;
18+
};
19+
20+
export type MutableServiceEnvPlan = {
21+
environment: Record<string, string | undefined>;
22+
environmentValueSources: Record<string, GatewayServiceEnvironmentValueSource | undefined>;
23+
entriesByNormalizedKey: Map<string, ServiceEnvPlanEntry>;
24+
};
25+
26+
export function createMutableServiceEnvPlan(): MutableServiceEnvPlan {
27+
return {
28+
environment: {},
29+
environmentValueSources: {},
30+
entriesByNormalizedKey: new Map(),
31+
};
32+
}
33+
34+
export function normalizeServiceEnvPlanKey(rawKey: string): string | undefined {
35+
return normalizeEnvVarKey(rawKey, { portable: true })?.toUpperCase();
36+
}
37+
38+
export function addServiceEnvPlanEntries(
39+
plan: MutableServiceEnvPlan,
40+
entries: Record<string, string | undefined>,
41+
options: {
42+
source: ServiceEnvSource;
43+
includeRawKeys?: boolean;
44+
valueSource?:
45+
| GatewayServiceEnvironmentValueSource
46+
| ((params: {
47+
rawKey: string;
48+
normalizedKey: string;
49+
}) => GatewayServiceEnvironmentValueSource | undefined);
50+
},
51+
): void {
52+
for (const [rawKey, rawValue] of Object.entries(entries)) {
53+
if (typeof rawValue !== "string" || !rawValue.trim()) {
54+
if (options.includeRawKeys) {
55+
plan.environment[rawKey] = rawValue;
56+
plan.environmentValueSources[rawKey] = "inline";
57+
}
58+
continue;
59+
}
60+
const value = rawValue;
61+
const normalizedKey = normalizeServiceEnvPlanKey(rawKey);
62+
if (!normalizedKey) {
63+
continue;
64+
}
65+
plan.environment[rawKey] = value;
66+
const valueSource =
67+
typeof options.valueSource === "function"
68+
? options.valueSource({ rawKey, normalizedKey })
69+
: options.valueSource;
70+
plan.environmentValueSources[rawKey] = valueSource ?? "inline";
71+
plan.entriesByNormalizedKey.set(normalizedKey, {
72+
rawKey,
73+
normalizedKey,
74+
value,
75+
source: options.source,
76+
});
77+
}
78+
}
79+
80+
export function compactServiceEnvPlanValueSources(plan: MutableServiceEnvPlan): void {
81+
for (const key of Object.keys(plan.environmentValueSources)) {
82+
if (!Object.hasOwn(plan.environment, key)) {
83+
delete plan.environmentValueSources[key];
84+
}
85+
}
86+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { MutableServiceEnvPlan } from "./service-env-plan.js";
2+
import {
3+
readManagedServiceEnvKeysFromEnvironment,
4+
writeManagedServiceEnvKeysToEnvironment,
5+
} from "./service-managed-env.js";
6+
7+
function isLaunchAgentServiceEnvironment(params: {
8+
platform: NodeJS.Platform;
9+
serviceEnvironment: Record<string, string | undefined>;
10+
}): boolean {
11+
return (
12+
params.platform === "darwin" &&
13+
Boolean(params.serviceEnvironment.OPENCLAW_LAUNCHD_LABEL?.trim())
14+
);
15+
}
16+
17+
export function applyManagedServiceEnvRenderPolicy(params: {
18+
plan: MutableServiceEnvPlan;
19+
managedServiceEnvKeys: string | undefined;
20+
serviceEnvironment: Record<string, string | undefined>;
21+
platform: NodeJS.Platform;
22+
}): void {
23+
writeManagedServiceEnvKeysToEnvironment(params.plan.environment, params.managedServiceEnvKeys);
24+
if (params.plan.environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS) {
25+
params.plan.environmentValueSources.OPENCLAW_SERVICE_MANAGED_ENV_KEYS = "inline";
26+
}
27+
if (!isLaunchAgentServiceEnvironment(params)) {
28+
return;
29+
}
30+
const managedKeys = readManagedServiceEnvKeysFromEnvironment({
31+
OPENCLAW_SERVICE_MANAGED_ENV_KEYS: params.managedServiceEnvKeys,
32+
});
33+
if (managedKeys.size === 0) {
34+
return;
35+
}
36+
for (const entry of params.plan.entriesByNormalizedKey.values()) {
37+
if (entry.source !== "state-dotenv" || !managedKeys.has(entry.normalizedKey)) {
38+
continue;
39+
}
40+
params.plan.environment[entry.rawKey] = entry.value;
41+
params.plan.environmentValueSources[entry.rawKey] = "inline";
42+
}
43+
}

0 commit comments

Comments
 (0)