diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d34383dd..fbb278403 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/cli/daemon-cli/install.ts b/src/cli/daemon-cli/install.ts index 469c11d69..6ac378368 100644 --- a/src/cli/daemon-cli/install.ts +++ b/src/cli/daemon-cli/install.ts @@ -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 { diff --git a/src/commands/configure.daemon.ts b/src/commands/configure.daemon.ts index 4e1b1c79c..38d8365c0 100644 --- a/src/commands/configure.daemon.ts +++ b/src/commands/configure.daemon.ts @@ -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…"); diff --git a/src/commands/daemon-install-helpers.test.ts b/src/commands/daemon-install-helpers.test.ts index 24a062c48..8cd819185 100644 --- a/src/commands/daemon-install-helpers.test.ts +++ b/src/commands/daemon-install-helpers.test.ts @@ -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"); }); }); diff --git a/src/commands/daemon-install-helpers.ts b/src/commands/daemon-install-helpers.ts index 9e386b865..21317ea2f 100644 --- a/src/commands/daemon-install-helpers.ts +++ b/src/commands/daemon-install-helpers.ts @@ -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; + /** Full config to extract env vars from (env vars + inline env keys). */ + config?: ClawdbotConfig; }): Promise { 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 = {}; - if (params.configEnvVars) { - for (const [key, value] of Object.entries(params.configEnvVars)) { - if (value) environment[key] = value; - } - } + const environment: Record = { + ...collectConfigEnvVars(params.config), + }; Object.assign(environment, serviceEnvironment); return { programArguments, workingDirectory, environment }; diff --git a/src/commands/doctor-gateway-daemon-flow.ts b/src/commands/doctor-gateway-daemon-flow.ts index 8623c5b3b..c209386f0 100644 --- a/src/commands/doctor-gateway-daemon-flow.ts +++ b/src/commands/doctor-gateway-daemon-flow.ts @@ -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({ diff --git a/src/commands/doctor-gateway-services.ts b/src/commands/doctor-gateway-services.ts index f5ecccade..7da0c5c0c 100644 --- a/src/commands/doctor-gateway-services.ts +++ b/src/commands/doctor-gateway-services.ts @@ -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); diff --git a/src/commands/onboard-non-interactive/local/daemon-install.ts b/src/commands/onboard-non-interactive/local/daemon-install.ts index 6f073db7f..68017b9e6 100644 --- a/src/commands/onboard-non-interactive/local/daemon-install.ts +++ b/src/commands/onboard-non-interactive/local/daemon-install.ts @@ -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({ diff --git a/src/config/env-vars.ts b/src/config/env-vars.ts new file mode 100644 index 000000000..7e9c5b158 --- /dev/null +++ b/src/config/env-vars.ts @@ -0,0 +1,23 @@ +import type { ClawdbotConfig } from "./types.js"; + +export function collectConfigEnvVars(cfg?: ClawdbotConfig): Record { + const envConfig = cfg?.env; + if (!envConfig) return {}; + + const entries: Record = {}; + + 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; +} diff --git a/src/config/io.ts b/src/config/io.ts index 6994e4485..da3a7fb23 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -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 = {}; - - 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; diff --git a/src/wizard/onboarding.finalize.ts b/src/wizard/onboarding.finalize.ts index 672833a26..32ab53dcf 100644 --- a/src/wizard/onboarding.finalize.ts +++ b/src/wizard/onboarding.finalize.ts @@ -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…");