openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
493 lines (492 loc) • 22 kB
JavaScript
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js";
import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js";
import { a as normalizeEnvVarKey, n as isDangerousHostEnvOverrideVarName, r as isDangerousHostEnvVarName } from "./host-env-security-RnaFYKhk.js";
import { n as collectDurableServiceEnvVars, t as collectDurableServiceEnvVarSources } from "./state-dir-dotenv-EUH5pibs.js";
import { m as resolveSecretInputRef, s as hasConfiguredSecretInput } from "./types.secrets-_0JOMGE5.js";
import { u as resolveGatewayLaunchAgentLabel } from "./constants-e049XCW0.js";
import { n as resolveGatewayTaskScriptPath, t as resolveGatewayStateDir } from "./paths-Qb6rRFUH.js";
import { a as OPENCLAW_WRAPPER_ENV_KEY, c as resolveOpenClawWrapperPath, i as resolveDaemonServicePathDirs, n as resolveDaemonInstallRuntimeInputs, o as resolveGatewayProgramArguments, t as emitDaemonInstallRuntimeWarning } from "./daemon-install-plan.shared-Dr7I1qWf.js";
import { C as writeManagedServiceEnvKeysToEnvironment, S as readManagedServiceEnvKeysFromEnvironment, y as formatManagedServiceEnvKeys } from "./systemd-BXTM6gEb.js";
import { d as buildServiceEnvironment, s as isNonMinimalServicePathEntry } from "./runtime-paths-DS_J3TGI.js";
import { t as loadPluginManifestRegistry } from "./manifest-registry-BywZIeAq.js";
import { l as resolveSecretProviderIntegrationConfig, s as isPluginIntegrationSecretProviderConfig } from "./resolve-DPFnV1p0.js";
import { r as createResolverContext } from "./runtime-shared-DR5938Io.js";
import { t as collectPluginConfigAssignments } from "./runtime-config-collectors-plugins-j14FBueI.js";
import { n as discoverConfigSecretTargets } from "./target-registry-0k0oK6Ia.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: {},
entriesByNormalizedKey: /* @__PURE__ */ new Map()
};
}
function normalizeServiceEnvPlanKey(rawKey) {
return normalizeEnvVarKey(rawKey, { portable: true })?.toUpperCase();
}
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 = 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
});
}
}
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 applyManagedServiceEnvRenderPolicy(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";
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";
}
}
//#endregion
//#region src/commands/daemon-install-helpers.ts
let daemonInstallAuthProfileSourceRuntimePromise;
let daemonInstallAuthProfileStoreRuntimePromise;
const NON_PERSISTED_CONFIG_SECRET_ENV_TARGET_IDS = new Set(["gateway.auth.password", "gateway.auth.token"]);
const EXEC_SECRET_REF_PASS_ENV_ALLOWED_OVERRIDE_ONLY_KEYS = new Set(["HOME"]);
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());
}
function loadDaemonInstallAuthProfileSourceRuntime() {
daemonInstallAuthProfileSourceRuntimePromise ??= import("./daemon-install-auth-profiles-source.runtime.js");
return daemonInstallAuthProfileSourceRuntimePromise;
}
function loadDaemonInstallAuthProfileStoreRuntime() {
daemonInstallAuthProfileStoreRuntimePromise ??= import("./daemon-install-auth-profiles-store.runtime.js");
return daemonInstallAuthProfileStoreRuntimePromise;
}
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;
}
function collectConfigSecretRefServiceEnvVars(params) {
if (!params.config) return {};
const entries = {};
for (const target of discoverConfigSecretTargets(params.config)) {
if (!target.entry.includeInPlan) continue;
if (NON_PERSISTED_CONFIG_SECRET_ENV_TARGET_IDS.has(target.entry.id)) 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 (Object.hasOwn(params.durableEnvironment, key)) continue;
const value = params.env[key]?.trim();
if (!value) continue;
entries[key] = value;
}
return entries;
}
function collectExecSecretRefPassEnvServiceEnvVars(params) {
if (!params.config) return {};
const entries = {};
let manifestRegistry;
const sources = [];
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"
});
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 ??= loadPluginManifestRegistry({
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;
}
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;
const value = params.env[key]?.trim();
if (!value) 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 = 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 readExistingEnvironmentValueSource(params) {
for (const [rawKey, source] of Object.entries(params.existingEnvironmentValueSources ?? {})) if (normalizeEnvVarKey(rawKey, { portable: true })?.toUpperCase() === params.normalizedKey) return source;
}
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 configSecretRefEnvironment = collectConfigSecretRefServiceEnvVars({
env: params.env,
config: params.config,
durableEnvironment,
warn: params.warn
});
const authStore = await resolveAuthProfileStoreForServiceEnv(params.authStore);
const execSecretRefPassEnvEnvironment = collectExecSecretRefPassEnvServiceEnvVars({
env: params.env,
config: params.config,
authStore,
durableEnvironment,
warn: params.warn
});
const authProfileEnvironment = collectAuthProfileServiceEnvVars({
env: params.env,
authStore,
warn: params.warn
});
const preservedExistingEnvironment = collectPreservedExistingServiceEnvVars(params.existingEnvironment, readManagedServiceEnvKeysFromEnvironment(params.existingEnvironment));
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" });
applyManagedServiceEnvRenderPolicy({
plan,
managedServiceEnvKeys: formatManagedServiceEnvKeys(durableEnvironment, { omitKeys: Object.keys(params.serviceEnvironment) }),
serviceEnvironment: params.serviceEnvironment,
platform: params.platform
});
addServiceEnvPlanEntries(plan, params.serviceEnvironment, {
source: "service-generated",
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 { devMode, nodePath } = await resolveDaemonInstallRuntimeInputs({
env: params.env,
runtime: params.runtime,
devMode: params.devMode,
nodePath: params.nodePath
});
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 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,
nodePath,
wrapperPath
});
await emitDaemonInstallRuntimeWarning({
env: params.env,
runtime: params.runtime,
programArguments,
warn: params.warn,
title: "Gateway runtime"
});
const serviceEnvironment = buildServiceEnvironment({
env: serviceInputEnv,
port: params.port,
launchdLabel: platform === "darwin" ? resolveGatewayLaunchAgentLabel(serviceInputEnv.OPENCLAW_PROFILE) : void 0,
platform,
extraPathDirs: resolveDaemonServicePathDirs({
nodePath,
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
//#region src/gateway/auth-install-policy.ts
function hasExplicitGatewayInstallAuthMode(mode) {
if (mode === "token") return true;
if (mode === "password" || mode === "none" || mode === "trusted-proxy") return false;
}
function hasConfiguredGatewayPasswordForInstall(cfg) {
return hasConfiguredSecretInput(cfg.gateway?.auth?.password, cfg.secrets?.defaults);
}
function hasDurableGatewayPasswordEnvForInstall(cfg, env) {
const durableServiceEnv = collectDurableServiceEnvVars({
env,
config: cfg
});
return Boolean(normalizeOptionalString(durableServiceEnv.OPENCLAW_GATEWAY_PASSWORD) || normalizeOptionalString(durableServiceEnv.CLAWDBOT_GATEWAY_PASSWORD));
}
/** Decide whether install should require token auth when no durable password source exists. */
function shouldRequireGatewayTokenForInstall(cfg, env) {
const explicitModeDecision = hasExplicitGatewayInstallAuthMode(cfg.gateway?.auth?.mode);
if (explicitModeDecision !== void 0) return explicitModeDecision;
if (hasConfiguredGatewayPasswordForInstall(cfg)) return false;
if (hasDurableGatewayPasswordEnvForInstall(cfg, env)) return false;
return true;
}
//#endregion
export { buildGatewayInstallPlan as n, gatewayInstallErrorHint as r, shouldRequireGatewayTokenForInstall as t };