fix: propagate config env vars to gateway services (#1735) (thanks @Seredeep)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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…");
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
23
src/config/env-vars.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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…");
|
||||
|
||||
Reference in New Issue
Block a user