openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
600 lines (599 loc) • 27.3 kB
JavaScript
import { l as resolveGatewayLaunchAgentLabel } from "./constants-ChqKLfPp.js";
import { n as createLazyPromise } from "./lazy-promise-DGqyc4Y4.js";
import { t as formatCliCommand } from "./command-format-C7YfyMTd.js";
import { s as coerceSecretRef, v as resolveSecretInputRef } from "./types.secrets-kC0nOetj.js";
import { a as normalizeEnvVarKey, n as isDangerousHostEnvOverrideVarName, r as isDangerousHostEnvVarName } from "./host-env-security-BAvlDaJf.js";
import { t as resolveConfigWidePluginManifestRegistry } from "./io.plugin-metadata-BruF5izX.js";
import { i as createResolverContext } from "./runtime-shared-CqbaocQu.js";
import { n as discoverConfigSecretTargets } from "./target-registry-query-qVhyD-Hq.js";
import "./target-registry-BHDUOLUU.js";
import { u as collectDurableServiceEnvVarSources } from "./systemd-service-files-BCcumv4T.js";
import { n as resolveGatewayStateDir, r as resolveGatewayTaskScriptPath } from "./paths-CzCbqt0l.js";
import { a as resolveManagedGatewayServiceCommand } from "./service-types-CK8rxZ9x.js";
import { i as resolveOpenClawWrapperPath, n as resolveGatewayProgramArguments, t as OPENCLAW_WRAPPER_ENV_KEY } from "./program-args-CrrbBbqY.js";
import { a as hasEnvironmentFileSource, c as normalizeServiceEnvKey, d as readManagedServiceEnvKeysFromEnvironment, i as formatManagedServiceEnvKeys, p as writeManagedServiceEnvKeysToEnvironment, u as readEnvironmentValueSource } from "./service-managed-env-j4Dxj-Tf.js";
import { l as isNonMinimalServicePathEntry, p as buildServiceEnvironment } from "./runtime-paths-BjNV6RBz.js";
import { c as resolveSecretProviderIntegrationConfig, o as isPluginIntegrationSecretProviderConfig } from "./resolve-224YoYfx.js";
import { t as collectPluginConfigAssignments } from "./runtime-config-collectors-plugins-4zRYF1Rd.js";
import { n as evaluateGatewayAuthSurfaceStates } from "./runtime-gateway-auth-surfaces-M-CtMPEM.js";
import { i as resolveDaemonServicePathDirs, n as resolveDaemonInstallRuntimeInputs, t as emitDaemonInstallRuntimeWarning } from "./daemon-install-plan.shared-BALGXKcf.js";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
//#region src/daemon/service-env-plan.ts
/** Builds normalized environment plans for managed daemon service rendering. */
function createMutableServiceEnvPlan() {
return {
environment: {},
environmentValueSources: {}
};
}
function addServiceEnvPlanEntries(plan, entries, options) {
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 = normalizeServiceEnvKey(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";
}
}
function compactServiceEnvPlanValueSources(plan) {
for (const key of Object.keys(plan.environmentValueSources)) if (!Object.hasOwn(plan.environment, key)) delete plan.environmentValueSources[key];
}
//#endregion
//#region src/daemon/service-env-render-policy.ts
function isLaunchAgentServiceEnvironment(params) {
return params.platform === "darwin" && Boolean(params.serviceEnvironment.OPENCLAW_LAUNCHD_LABEL?.trim());
}
function addManagedServiceEnvEntries(params) {
for (const [rawKey, value] of Object.entries(params.entries)) {
if (typeof value !== "string" || !value.trim()) continue;
const key = normalizeServiceEnvKey(rawKey);
if (!key || !params.managedKeys.has(key)) continue;
params.plan.environment[rawKey] = value;
params.plan.environmentValueSources[rawKey] = params.valueSource;
}
}
function applyManagedServiceEnvRenderPolicy(params) {
const launchAgent = isLaunchAgentServiceEnvironment(params);
writeManagedServiceEnvKeysToEnvironment(params.plan.environment, params.managedServiceEnvKeys);
if (params.plan.environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS) params.plan.environmentValueSources.OPENCLAW_SERVICE_MANAGED_ENV_KEYS = "inline";
const managedKeys = readManagedServiceEnvKeysFromEnvironment({ OPENCLAW_SERVICE_MANAGED_ENV_KEYS: params.managedServiceEnvKeys });
if (managedKeys.size === 0) return;
if (launchAgent || params.platform === "linux") addManagedServiceEnvEntries({
plan: params.plan,
entries: params.existingEnvironmentFileEnvironment,
managedKeys,
valueSource: "file"
});
if (launchAgent) addManagedServiceEnvEntries({
plan: params.plan,
entries: params.stateDirDotEnvEnvironment,
managedKeys,
valueSource: "inline"
});
addManagedServiceEnvEntries({
plan: params.plan,
entries: params.configSecretRefEnvironment,
managedKeys,
valueSource: params.platform === "linux" ? "file" : "inline"
});
}
//#endregion
//#region src/commands/daemon-install-helpers.ts
const NON_PERSISTED_CONFIG_SECRET_ENV_TARGET_IDS = /* @__PURE__ */ new Set(["gateway.auth.password", "gateway.auth.token"]);
const EXEC_SECRET_REF_PASS_ENV_ALLOWED_OVERRIDE_ONLY_KEYS = /* @__PURE__ */ new Set(["HOME"]);
function configContainsSecretRef(config) {
if (!config) return false;
const pending = [config];
const seen = /* @__PURE__ */ new Set();
const defaults = config.secrets?.defaults;
while (pending.length > 0) {
const value = pending.pop();
if (coerceSecretRef(value, defaults)) return true;
if (!value || typeof value !== "object" || seen.has(value)) continue;
seen.add(value);
pending.push(...Object.values(value));
}
return false;
}
function isBlockedExecSecretRefPassEnvKey(key) {
if (isDangerousHostEnvVarName(key)) return true;
if (!isDangerousHostEnvOverrideVarName(key)) return false;
return !EXEC_SECRET_REF_PASS_ENV_ALLOWED_OVERRIDE_ONLY_KEYS.has(key.toUpperCase());
}
const loadDaemonInstallAuthProfileSourceRuntime = createLazyPromise(() => import("./daemon-install-auth-profiles-source.runtime.js"), { cacheRejections: true });
const loadDaemonInstallAuthProfileStoreRuntime = createLazyPromise(() => import("./daemon-install-auth-profiles-store.runtime.js"), { cacheRejections: true });
const loadDaemonInstallProviderManifestRuntime = createLazyPromise(() => import("./manifest-contract-eligibility-DUmK-3bG.js"), { cacheRejections: true });
async function resolveAuthProfileStoreForServiceEnv(authStore) {
if (authStore) return authStore;
const { hasAnyAuthProfileStoreSource } = await loadDaemonInstallAuthProfileSourceRuntime();
if (!hasAnyAuthProfileStoreSource()) return;
const { loadAuthProfileStoreForSecretsRuntime } = await loadDaemonInstallAuthProfileStoreRuntime();
return loadAuthProfileStoreForSecretsRuntime();
}
function collectAuthProfileSecretRefs(authStore) {
if (!authStore) return [];
const refs = [];
for (const credential of Object.values(authStore.profiles)) {
const ref = credential.type === "api_key" ? credential.keyRef : credential.type === "token" ? credential.tokenRef : void 0;
if (ref) refs.push(ref);
}
return refs;
}
function collectAuthProfileServiceEnvVars(params) {
const entries = {};
for (const ref of collectAuthProfileSecretRefs(params.authStore)) {
if (!ref || ref.source !== "env") continue;
const key = normalizeEnvVarKey(ref.id, { portable: true });
if (!key) continue;
if (isDangerousHostEnvVarName(key) || isDangerousHostEnvOverrideVarName(key)) {
params.warn?.(`Auth profile env ref "${key}" blocked by host-env security policy`, "Auth profile");
continue;
}
const value = params.env[key]?.trim();
if (!value) continue;
entries[key] = value;
}
return entries;
}
async function collectAmbientProviderApiKeyServiceEnvVars(params) {
if (params.platform !== "linux") return {};
const existingManagedKeys = readManagedServiceEnvKeysFromEnvironment(params.existingEnvironment);
const ownedKeys = new Set([
...Object.keys(params.durableEnvironment),
...Object.keys(params.authProfileEnvironment),
...Object.entries(params.existingEnvironment ?? {}).flatMap(([key, value]) => existingManagedKeys.has(key.toUpperCase()) && params.env[key]?.trim() !== value?.trim() ? [] : [key])
].map((key) => key.toUpperCase()));
const candidates = new Map(Object.entries(params.env).flatMap(([rawKey, rawValue]) => {
const key = normalizeEnvVarKey(rawKey, { portable: true })?.toUpperCase();
const value = rawValue?.trim();
return key && key.endsWith("_API_KEY") && !key.endsWith("_ADMIN_API_KEY") && !ownedKeys.has(key) && value && !isDangerousHostEnvVarName(key) && !isDangerousHostEnvOverrideVarName(key) ? [[key, value]] : [];
}));
if (candidates.size === 0) return {};
const { isManifestPluginAvailableForControlPlane, loadManifestMetadataSnapshot } = await loadDaemonInstallProviderManifestRuntime();
const config = params.config ?? {};
const snapshot = loadManifestMetadataSnapshot({
config,
env: params.env
});
return Object.fromEntries(snapshot.plugins.flatMap((plugin) => {
if (plugin.origin !== "bundled" && plugin.trustedOfficialInstall !== true || !isManifestPluginAvailableForControlPlane({
snapshot,
plugin,
config
})) return [];
const providers = new Set((plugin.providerAuthChoices ?? []).filter(({ method, appGuidedSecret, onboardingScopes }) => method === "api-key" && appGuidedSecret === true && (!onboardingScopes || onboardingScopes.includes("text-inference"))).map(({ provider }) => provider));
return (plugin.setup?.providers ?? []).filter(({ id }) => providers.has(id)).flatMap(({ envVars = [] }) => envVars.flatMap((name) => {
const key = normalizeEnvVarKey(name, { portable: true })?.toUpperCase();
const value = key ? candidates.get(key) : void 0;
return key && value ? [[key, value]] : [];
}));
}));
}
function collectConfigSecretRefServiceEnvSources(params) {
const keys = /* @__PURE__ */ new Set();
const environment = {};
if (!params.config || !params.configContainsSecretRef) return {
keys: [],
environment
};
const gatewayAuthSurfaceStates = evaluateGatewayAuthSurfaceStates({
config: params.config,
env: params.env,
defaults: params.config.secrets?.defaults
});
for (const target of discoverConfigSecretTargets(params.config)) {
if (!target.entry.includeInPlan) continue;
const { ref } = resolveSecretInputRef({
value: target.value,
refValue: target.refValue,
defaults: params.config.secrets?.defaults
});
if (!ref || ref.source !== "env") continue;
const key = normalizeEnvVarKey(ref.id, { portable: true });
if (!key) {
params.warn?.(`Config SecretRef env id "${ref.id}" is not portable and was not added to the service environment`, "Config SecretRef");
continue;
}
if (isDangerousHostEnvVarName(key) || isDangerousHostEnvOverrideVarName(key)) {
params.warn?.(`Config SecretRef env ref "${key}" blocked by host-env security policy`, "Config SecretRef");
continue;
}
if (NON_PERSISTED_CONFIG_SECRET_ENV_TARGET_IDS.has(target.entry.id)) {
if (gatewayAuthSurfaceStates[target.entry.id]?.active) keys.add(key.toUpperCase());
continue;
}
keys.add(key.toUpperCase());
if (Object.hasOwn(params.stateDirDotEnvEnvironment, key)) continue;
const value = params.env[key]?.trim();
if (!value) continue;
environment[key] = value;
}
return {
keys: [...keys],
environment
};
}
function collectExecSecretRefPassEnvServiceEnvVars(params) {
if (!params.config) return {};
const entries = {};
let manifestRegistry;
const sources = [];
if (params.configContainsSecretRef) for (const target of discoverConfigSecretTargets(params.config)) {
if (!target.entry.includeInPlan) continue;
const { ref } = resolveSecretInputRef({
value: target.value,
refValue: target.refValue,
defaults: params.config.secrets?.defaults
});
if (!ref || ref.source !== "exec") continue;
sources.push({
ref,
warningTitle: "Config SecretRef"
});
}
for (const ref of collectAuthProfileSecretRefs(params.authStore)) if (ref.source === "exec") sources.push({
ref,
warningTitle: "Auth profile"
});
if (params.configContainsSecretRef) {
for (const ref of collectPluginConfigSecretRefs({
env: params.env,
config: params.config
})) if (ref.source === "exec") sources.push({
ref,
warningTitle: "Plugin config SecretRef"
});
}
for (const { ref, warningTitle } of sources) {
const provider = params.config.secrets?.providers?.[ref.provider];
if (!provider || provider.source !== "exec") continue;
const execProvider = isPluginIntegrationSecretProviderConfig(provider) ? (() => {
manifestRegistry ??= resolveConfigWidePluginManifestRegistry({
config: params.config,
env: params.env
});
const resolved = resolveSecretProviderIntegrationConfig({
manifestRegistry,
providerAlias: ref.provider,
providerConfig: provider,
config: params.config,
env: params.env
});
if (!resolved.ok) {
params.warn?.(`Exec SecretRef plugin provider "${ref.provider}" could not be resolved for service environment planning: ${resolved.reason}`, warningTitle);
return;
}
return resolved.providerConfig;
})() : provider;
if (!execProvider) continue;
for (const rawKey of execProvider.passEnv ?? []) {
const key = normalizeEnvVarKey(rawKey, { portable: true });
if (!key) {
params.warn?.(`Exec SecretRef passEnv id "${rawKey}" is not portable and was not added to the service environment`, warningTitle);
continue;
}
const value = Object.hasOwn(params.env, key) ? params.env[key]?.trim() : void 0;
if (!value) continue;
if (isBlockedExecSecretRefPassEnvKey(key)) {
params.warn?.(`Exec SecretRef passEnv ref "${key}" blocked by host-env security policy`, warningTitle);
continue;
}
if (Object.hasOwn(params.durableEnvironment, key)) continue;
entries[key] = value;
}
}
return entries;
}
function collectPluginConfigSecretRefs(params) {
const context = createResolverContext({
sourceConfig: params.config,
env: params.env
});
collectPluginConfigAssignments({
config: params.config,
defaults: params.config.secrets?.defaults,
context
});
return context.assignments.map((assignment) => assignment.ref);
}
function mergeServicePath(nextPath, existingPath, tmpDir, platform) {
const segments = [];
const seen = /* @__PURE__ */ new Set();
const normalizedTmpDirs = [tmpDir, os.tmpdir()].map((value) => value?.trim()).filter((value) => Boolean(value)).map((value) => path.resolve(value));
const realTmpDirs = normalizedTmpDirs.map((tmpRoot) => {
try {
return path.normalize(fs.realpathSync.native(tmpRoot));
} catch {
return tmpRoot;
}
});
const isSameOrChildPath = (candidate, parent) => candidate === parent || candidate.startsWith(`${parent}${path.sep}`);
const isUnsafeProcPath = (candidate) => candidate === `${path.sep}proc` || candidate.startsWith(`${path.sep}proc${path.sep}`);
const realpathExistingPath = (candidate) => {
const parts = [];
let current = candidate;
while (current && current !== path.dirname(current)) try {
const realCurrent = path.normalize(fs.realpathSync.native(current));
return path.normalize(path.join(realCurrent, ...parts.toReversed()));
} catch {
parts.push(path.basename(current));
current = path.dirname(current);
}
try {
return path.normalize(path.join(fs.realpathSync.native(current), ...parts.toReversed()));
} catch {
return;
}
};
const normalizePreservedPathSegment = (segment) => {
if (!path.isAbsolute(segment)) return;
const normalized = path.normalize(segment);
if (isUnsafeProcPath(normalized)) return;
const cwd = path.resolve(process.cwd());
if (isSameOrChildPath(normalized, cwd)) return;
try {
const realSegment = realpathExistingPath(normalized);
const realCwd = path.normalize(fs.realpathSync.native(cwd));
if (realSegment && isSameOrChildPath(realSegment, realCwd)) return;
} catch {}
return normalized;
};
const shouldPreserveNormalizedPathSegment = (segment) => {
if (isNonMinimalServicePathEntry(segment, platform)) return false;
const resolved = path.resolve(segment);
const realResolved = realpathExistingPath(resolved) ?? resolved;
return ![...normalizedTmpDirs, ...realTmpDirs].some((tmpRoot) => isSameOrChildPath(resolved, tmpRoot) || isSameOrChildPath(realResolved, tmpRoot));
};
const addPath = (value, options) => {
if (typeof value !== "string" || value.trim().length === 0) return;
for (const segment of value.split(path.delimiter)) {
const trimmed = segment.trim();
const candidate = options?.preserve ? normalizePreservedPathSegment(trimmed) : trimmed;
if (options?.preserve && (!candidate || !shouldPreserveNormalizedPathSegment(candidate))) continue;
if (!candidate || seen.has(candidate)) continue;
seen.add(candidate);
segments.push(candidate);
}
};
addPath(nextPath);
if (platform !== "darwin") addPath(existingPath, { preserve: true });
return segments.length > 0 ? segments.join(path.delimiter) : void 0;
}
const PRESERVED_OPENCLAW_OPERATOR_OPT_IN_ENV_KEYS = /* @__PURE__ */ new Set(["OPENCLAW_CLI_CONTAINER_BYPASS", "OPENCLAW_CONTAINER_HINT"]);
/** Preserve safe operator-owned env vars from an existing service definition. */
function collectPreservedExistingServiceEnvVars(existingEnvironment, managedServiceEnvKeys) {
if (!existingEnvironment) return {};
const preserved = {};
for (const [rawKey, rawValue] of Object.entries(existingEnvironment)) {
const key = normalizeEnvVarKey(rawKey, { portable: true });
if (!key) continue;
const upper = key.toUpperCase();
if (upper === "HOME" || upper === "PATH" || upper === "TMPDIR" || upper.startsWith("OPENCLAW_") && !PRESERVED_OPENCLAW_OPERATOR_OPT_IN_ENV_KEYS.has(upper)) continue;
if (managedServiceEnvKeys.has(upper)) continue;
if (isDangerousHostEnvVarName(key) || isDangerousHostEnvOverrideVarName(key)) continue;
const value = rawValue?.trim();
if (!value) continue;
preserved[key] = value;
}
return preserved;
}
function collectExistingEnvironmentFileManagedServiceEnvVars(params) {
if (!params.existingEnvironment || params.configSecretRefKeys.size === 0) return {};
const preserved = {};
for (const [rawKey, rawValue] of Object.entries(params.existingEnvironment)) {
const key = normalizeEnvVarKey(rawKey, { portable: true });
if (!key) continue;
const normalizedKey = key.toUpperCase();
if (!params.configSecretRefKeys.has(normalizedKey)) continue;
if (isDangerousHostEnvVarName(key) || isDangerousHostEnvOverrideVarName(key)) continue;
const source = readEnvironmentValueSource(params.existingEnvironmentValueSources, normalizedKey);
if (!hasEnvironmentFileSource(source)) continue;
const value = rawValue?.trim();
if (!value) continue;
preserved[key] = value;
}
return preserved;
}
function omitEnvironmentEntriesShadowedBy(entries, shadowEntries) {
const shadowKeys = new Set(shadowEntries.flatMap((environment) => Object.keys(environment).flatMap((key) => {
const normalized = normalizeEnvVarKey(key, { portable: true })?.toUpperCase();
return normalized ? [normalized] : [];
})));
return Object.fromEntries(Object.entries(entries).filter(([key]) => {
const normalized = normalizeEnvVarKey(key, { portable: true })?.toUpperCase();
return !normalized || !shadowKeys.has(normalized);
}));
}
function resolveGatewayInstallWorkingDirectory(params) {
if (params.workingDirectory) return params.workingDirectory;
if (params.platform !== "darwin") return;
return resolveGatewayStateDir(params.env);
}
async function buildGatewayInstallEnvironment(params) {
const { stateDirDotEnvEnvironment, configEnvironment, durableEnvironment } = collectDurableServiceEnvVarSources({
env: params.env,
config: params.config
});
const containsConfigSecretRef = configContainsSecretRef(params.config);
const { keys: configSecretRefKeys, environment: configSecretRefEnvironment } = collectConfigSecretRefServiceEnvSources({
env: params.env,
config: params.config,
configContainsSecretRef: containsConfigSecretRef,
stateDirDotEnvEnvironment,
warn: params.warn
});
const authStore = await resolveAuthProfileStoreForServiceEnv(params.authStore);
const execSecretRefPassEnvEnvironment = collectExecSecretRefPassEnvServiceEnvVars({
env: params.env,
config: params.config,
configContainsSecretRef: containsConfigSecretRef,
authStore,
durableEnvironment,
warn: params.warn
});
const authProfileEnvironment = collectAuthProfileServiceEnvVars({
env: params.env,
authStore,
warn: params.warn
});
const ambientProviderApiKeyEnvironment = await collectAmbientProviderApiKeyServiceEnvVars({
env: params.env,
config: params.config,
durableEnvironment,
authProfileEnvironment,
existingEnvironment: params.existingEnvironment,
platform: params.platform
});
const stateDirDotEnvRenderEnvironment = omitEnvironmentEntriesShadowedBy(stateDirDotEnvEnvironment, [
configEnvironment,
configSecretRefEnvironment,
execSecretRefPassEnvEnvironment,
authProfileEnvironment
]);
const preservedExistingEnvironment = collectPreservedExistingServiceEnvVars(params.existingEnvironment, readManagedServiceEnvKeysFromEnvironment(params.existingEnvironment));
const plan = createMutableServiceEnvPlan();
addServiceEnvPlanEntries(plan, preservedExistingEnvironment, { valueSource: ({ normalizedKey }) => readEnvironmentValueSource(params.existingEnvironmentValueSources, normalizedKey) ?? "inline" });
addServiceEnvPlanEntries(plan, ambientProviderApiKeyEnvironment, { valueSource: "file" });
addServiceEnvPlanEntries(plan, stateDirDotEnvEnvironment, {});
addServiceEnvPlanEntries(plan, configEnvironment, {});
addServiceEnvPlanEntries(plan, configSecretRefEnvironment, {});
addServiceEnvPlanEntries(plan, execSecretRefPassEnvEnvironment, {});
addServiceEnvPlanEntries(plan, authProfileEnvironment, {});
const configSecretRefKeyEnvironment = Object.fromEntries(configSecretRefKeys.map((key) => [key, "1"]));
const managedServiceEnvKeys = formatManagedServiceEnvKeys({
...durableEnvironment,
...configSecretRefKeyEnvironment,
...configSecretRefEnvironment
}, { omitKeys: Object.keys(params.serviceEnvironment) });
const existingEnvironmentFileRenderEnvironment = omitEnvironmentEntriesShadowedBy(collectExistingEnvironmentFileManagedServiceEnvVars({
existingEnvironment: params.existingEnvironment,
existingEnvironmentValueSources: params.existingEnvironmentValueSources,
configSecretRefKeys: new Set(configSecretRefKeys)
}), [
stateDirDotEnvRenderEnvironment,
configSecretRefEnvironment,
execSecretRefPassEnvEnvironment,
authProfileEnvironment
]);
applyManagedServiceEnvRenderPolicy({
plan,
managedServiceEnvKeys,
serviceEnvironment: params.serviceEnvironment,
platform: params.platform,
existingEnvironmentFileEnvironment: existingEnvironmentFileRenderEnvironment,
stateDirDotEnvEnvironment: stateDirDotEnvRenderEnvironment,
configSecretRefEnvironment
});
addServiceEnvPlanEntries(plan, params.serviceEnvironment, { includeRawKeys: true });
const mergedPath = mergeServicePath(params.serviceEnvironment.PATH, params.existingEnvironment?.PATH, params.serviceEnvironment.TMPDIR, params.platform);
if (mergedPath) {
plan.environment.PATH = mergedPath;
plan.environmentValueSources.PATH = "inline";
}
compactServiceEnvPlanValueSources(plan);
return {
environment: plan.environment,
environmentValueSources: plan.environmentValueSources
};
}
/** Build command, working directory, and environment for installing the Gateway service. */
async function buildGatewayInstallPlan(params) {
const platform = params.platform ?? process.platform;
const wrapperInput = params.wrapperPath ?? params.env["OPENCLAW_WRAPPER"];
const wrapperPointsAtWindowsTaskScript = Boolean(wrapperInput?.trim()) && platform === "win32" && isSameServicePath(wrapperInput, resolveGatewayTaskScriptPath(params.env), platform);
if (wrapperPointsAtWindowsTaskScript) params.warn?.(`Ignoring ${OPENCLAW_WRAPPER_ENV_KEY} because it points to the Windows task script; using the OpenClaw gateway entrypoint directly to avoid a recursive gateway.cmd wrapper.`);
const wrapperPath = wrapperPointsAtWindowsTaskScript ? void 0 : await resolveOpenClawWrapperPath(wrapperInput);
const { devMode, runtimePath } = await resolveDaemonInstallRuntimeInputs({
env: params.env,
runtime: params.runtime,
devMode: params.devMode,
runtimePath: params.runtimePath,
wrapperPath
});
const serviceInputEnv = wrapperPath ? {
...params.env,
[OPENCLAW_WRAPPER_ENV_KEY]: wrapperPath
} : wrapperPointsAtWindowsTaskScript ? omitEnvKey(params.env, OPENCLAW_WRAPPER_ENV_KEY) : params.env;
const { programArguments, workingDirectory } = await resolveGatewayProgramArguments({
port: params.port,
dev: devMode,
runtime: params.runtime,
runtimePath,
wrapperPath,
...params.existingCommand ? { existingCommand: params.existingCommand } : {}
});
await emitDaemonInstallRuntimeWarning({
env: params.env,
runtime: params.runtime,
programArguments,
warn: params.warn,
title: "Gateway runtime"
});
const serviceEnvironment = buildServiceEnvironment({
env: serviceInputEnv,
port: params.port,
runtime: params.runtime,
existingNodeOptions: resolveManagedGatewayServiceCommand(params.existingCommand)?.environment?.NODE_OPTIONS,
launchdLabel: platform === "darwin" ? resolveGatewayLaunchAgentLabel(serviceInputEnv.OPENCLAW_PROFILE) : void 0,
platform,
extraPathDirs: resolveDaemonServicePathDirs({
runtimePath,
env: serviceInputEnv,
platform
})
});
const { environment, environmentValueSources } = await buildGatewayInstallEnvironment({
env: serviceInputEnv,
config: params.config,
authStore: params.authStore,
warn: params.warn,
serviceEnvironment,
existingEnvironment: params.existingEnvironment,
existingEnvironmentValueSources: params.existingEnvironmentValueSources,
platform
});
return {
programArguments,
workingDirectory: resolveGatewayInstallWorkingDirectory({
env: serviceInputEnv,
platform,
workingDirectory
}),
environment,
...Object.keys(environmentValueSources).length > 0 ? { environmentValueSources } : {}
};
}
function normalizeServicePathForCompare(value, platform) {
const trimmed = value?.trim();
if (!trimmed) return;
return platform === "win32" ? path.win32.resolve(trimmed).toLowerCase() : path.resolve(trimmed);
}
function isSameServicePath(left, right, platform) {
const normalizedLeft = normalizeServicePathForCompare(left, platform);
const normalizedRight = normalizeServicePathForCompare(right, platform);
return Boolean(normalizedLeft && normalizedRight && normalizedLeft === normalizedRight);
}
function omitEnvKey(env, key) {
const next = { ...env };
delete next[key];
return next;
}
/** Return the user-facing recovery hint for failed Gateway service installation. */
function gatewayInstallErrorHint(platform = process.platform) {
return platform === "win32" ? "Tip: native Windows now falls back to a per-user Startup-folder login item when Scheduled Task creation is denied; if install still fails, rerun from an elevated PowerShell or skip service install." : `Tip: rerun \`${formatCliCommand("openclaw gateway install")}\` after fixing the error.`;
}
//#endregion
export { gatewayInstallErrorHint as n, buildGatewayInstallPlan as t };