fix: propagate config env vars to gateway services (#1735) (thanks @Seredeep)

This commit is contained in:
Peter Steinberger
2026-01-25 10:35:43 +00:00
parent f29f51569a
commit 737037129e
11 changed files with 122 additions and 44 deletions

View File

@@ -24,6 +24,7 @@ Docs: https://docs.clawd.bot
- Telegram: add verbose raw-update logging for inbound Telegram updates. (#1597) Thanks @rohannagpal.
### Fixes
- Gateway: include inline config env vars in service install environments. (#1735) Thanks @Seredeep.
- BlueBubbles: route phone-number targets to DMs, avoid leaking routing IDs, and auto-create missing DMs (Private API required). (#1751) Thanks @tyler6204. https://docs.clawd.bot/channels/bluebubbles
- BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing.
- Web UI: hide internal `message_id` hints in chat bubbles.

View File

@@ -100,7 +100,7 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
if (json) warnings.push(message);
else defaultRuntime.log(message);
},
configEnvVars: cfg.env?.vars,
config: cfg,
});
try {

View File

@@ -89,7 +89,7 @@ export async function maybeInstallDaemon(params: {
token: params.gatewayToken,
runtime: daemonRuntime,
warn: (message, title) => note(message, title),
configEnvVars: cfg.env?.vars,
config: cfg,
});
progress.setLabel("Installing Gateway service…");

View File

@@ -96,7 +96,7 @@ describe("buildGatewayInstallPlan", () => {
expect(mocks.resolvePreferredNodePath).toHaveBeenCalled();
});
it("merges configEnvVars into the environment", async () => {
it("merges config env vars into the environment", async () => {
mocks.resolvePreferredNodePath.mockResolvedValue("/opt/node");
mocks.resolveGatewayProgramArguments.mockResolvedValue({
programArguments: ["node", "gateway"],
@@ -116,9 +116,13 @@ describe("buildGatewayInstallPlan", () => {
env: {},
port: 3000,
runtime: "node",
configEnvVars: {
GOOGLE_API_KEY: "test-key",
CUSTOM_VAR: "custom-value",
config: {
env: {
vars: {
GOOGLE_API_KEY: "test-key",
},
CUSTOM_VAR: "custom-value",
},
},
});
@@ -130,7 +134,7 @@ describe("buildGatewayInstallPlan", () => {
expect(plan.environment.HOME).toBe("/Users/me");
});
it("does not include empty configEnvVars values", async () => {
it("does not include empty config env values", async () => {
mocks.resolvePreferredNodePath.mockResolvedValue("/opt/node");
mocks.resolveGatewayProgramArguments.mockResolvedValue({
programArguments: ["node", "gateway"],
@@ -147,16 +151,83 @@ describe("buildGatewayInstallPlan", () => {
env: {},
port: 3000,
runtime: "node",
configEnvVars: {
VALID_KEY: "valid",
EMPTY_KEY: "",
UNDEFINED_KEY: undefined,
config: {
env: {
vars: {
VALID_KEY: "valid",
EMPTY_KEY: "",
},
},
},
});
expect(plan.environment.VALID_KEY).toBe("valid");
expect(plan.environment.EMPTY_KEY).toBeUndefined();
expect(plan.environment.UNDEFINED_KEY).toBeUndefined();
});
it("drops whitespace-only config env values", async () => {
mocks.resolvePreferredNodePath.mockResolvedValue("/opt/node");
mocks.resolveGatewayProgramArguments.mockResolvedValue({
programArguments: ["node", "gateway"],
workingDirectory: "/Users/me",
});
mocks.resolveSystemNodeInfo.mockResolvedValue({
path: "/opt/node",
version: "22.0.0",
supported: true,
});
mocks.buildServiceEnvironment.mockReturnValue({});
const plan = await buildGatewayInstallPlan({
env: {},
port: 3000,
runtime: "node",
config: {
env: {
vars: {
VALID_KEY: "valid",
},
TRIMMED_KEY: " ",
},
},
});
expect(plan.environment.VALID_KEY).toBe("valid");
expect(plan.environment.TRIMMED_KEY).toBeUndefined();
});
it("keeps service env values over config env vars", async () => {
mocks.resolvePreferredNodePath.mockResolvedValue("/opt/node");
mocks.resolveGatewayProgramArguments.mockResolvedValue({
programArguments: ["node", "gateway"],
workingDirectory: "/Users/me",
});
mocks.resolveSystemNodeInfo.mockResolvedValue({
path: "/opt/node",
version: "22.0.0",
supported: true,
});
mocks.buildServiceEnvironment.mockReturnValue({
HOME: "/Users/service",
CLAWDBOT_PORT: "3000",
});
const plan = await buildGatewayInstallPlan({
env: {},
port: 3000,
runtime: "node",
config: {
env: {
HOME: "/Users/config",
vars: {
CLAWDBOT_PORT: "9999",
},
},
},
});
expect(plan.environment.HOME).toBe("/Users/service");
expect(plan.environment.CLAWDBOT_PORT).toBe("3000");
});
});

View File

@@ -7,6 +7,8 @@ import {
} from "../daemon/runtime-paths.js";
import { buildServiceEnvironment } from "../daemon/service-env.js";
import { formatCliCommand } from "../cli/command-format.js";
import { collectConfigEnvVars } from "../config/env-vars.js";
import type { ClawdbotConfig } from "../config/types.js";
import type { GatewayDaemonRuntime } from "./daemon-runtime.js";
type WarnFn = (message: string, title?: string) => void;
@@ -31,8 +33,8 @@ export async function buildGatewayInstallPlan(params: {
devMode?: boolean;
nodePath?: string;
warn?: WarnFn;
/** Environment variables from config.env.vars to include in the service environment */
configEnvVars?: Record<string, string | undefined>;
/** Full config to extract env vars from (env vars + inline env keys). */
config?: ClawdbotConfig;
}): Promise<GatewayInstallPlan> {
const devMode = params.devMode ?? resolveGatewayDevMode();
const nodePath =
@@ -62,14 +64,11 @@ export async function buildGatewayInstallPlan(params: {
: undefined,
});
// Merge config.env.vars into the service environment.
// Merge config env vars into the service environment (vars + inline env keys).
// Config env vars are added first so service-specific vars take precedence.
const environment: Record<string, string | undefined> = {};
if (params.configEnvVars) {
for (const [key, value] of Object.entries(params.configEnvVars)) {
if (value) environment[key] = value;
}
}
const environment: Record<string, string | undefined> = {
...collectConfigEnvVars(params.config),
};
Object.assign(environment, serviceEnvironment);
return { programArguments, workingDirectory, environment };

View File

@@ -161,7 +161,7 @@ export async function maybeRepairGatewayDaemon(params: {
token: params.cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN,
runtime: daemonRuntime,
warn: (message, title) => note(message, title),
configEnvVars: params.cfg.env?.vars,
config: params.cfg,
});
try {
await service.install({

View File

@@ -110,7 +110,7 @@ export async function maybeMigrateLegacyGatewayService(
token: cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN,
runtime: daemonRuntime,
warn: (message, title) => note(message, title),
configEnvVars: cfg.env?.vars,
config: cfg,
});
try {
await service.install({
@@ -178,7 +178,7 @@ export async function maybeRepairGatewayServiceConfig(
runtime: needsNodeRuntime && systemNodePath ? "node" : runtimeChoice,
nodePath: systemNodePath ?? undefined,
warn: (message, title) => note(message, title),
configEnvVars: cfg.env?.vars,
config: cfg,
});
const expectedEntrypoint = findGatewayEntrypoint(programArguments);
const currentEntrypoint = findGatewayEntrypoint(command.programArguments);

View File

@@ -38,7 +38,7 @@ export async function installGatewayDaemonNonInteractive(params: {
token: gatewayToken,
runtime: daemonRuntimeRaw,
warn: (message) => runtime.log(message),
configEnvVars: params.nextConfig.env?.vars,
config: params.nextConfig,
});
try {
await service.install({

23
src/config/env-vars.ts Normal file
View File

@@ -0,0 +1,23 @@
import type { ClawdbotConfig } from "./types.js";
export function collectConfigEnvVars(cfg?: ClawdbotConfig): Record<string, string> {
const envConfig = cfg?.env;
if (!envConfig) return {};
const entries: Record<string, string> = {};
if (envConfig.vars) {
for (const [key, value] of Object.entries(envConfig.vars)) {
if (!value) continue;
entries[key] = value;
}
}
for (const [key, value] of Object.entries(envConfig)) {
if (key === "shellEnv" || key === "vars") continue;
if (typeof value !== "string" || !value.trim()) continue;
entries[key] = value;
}
return entries;
}

View File

@@ -24,6 +24,7 @@ import {
} from "./defaults.js";
import { VERSION } from "../version.js";
import { MissingEnvVarError, resolveConfigEnvVars } from "./env-substitution.js";
import { collectConfigEnvVars } from "./env-vars.js";
import { ConfigIncludeError, resolveConfigIncludes } from "./includes.js";
import { findLegacyConfigIssues } from "./legacy.js";
import { normalizeConfigPaths } from "./normalize-paths.js";
@@ -149,24 +150,7 @@ function warnIfConfigFromFuture(cfg: ClawdbotConfig, logger: Pick<typeof console
}
function applyConfigEnv(cfg: ClawdbotConfig, env: NodeJS.ProcessEnv): void {
const envConfig = cfg.env;
if (!envConfig) return;
const entries: Record<string, string> = {};
if (envConfig.vars) {
for (const [key, value] of Object.entries(envConfig.vars)) {
if (!value) continue;
entries[key] = value;
}
}
for (const [key, value] of Object.entries(envConfig)) {
if (key === "shellEnv" || key === "vars") continue;
if (typeof value !== "string" || !value.trim()) continue;
entries[key] = value;
}
const entries = collectConfigEnvVars(cfg);
for (const [key, value] of Object.entries(entries)) {
if (env[key]?.trim()) continue;
env[key] = value;

View File

@@ -169,7 +169,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
token: settings.gatewayToken,
runtime: daemonRuntime,
warn: (message, title) => prompter.note(message, title),
configEnvVars: nextConfig.env?.vars,
config: nextConfig,
});
progress.update("Installing Gateway service…");