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.
|
- Telegram: add verbose raw-update logging for inbound Telegram updates. (#1597) Thanks @rohannagpal.
|
||||||
|
|
||||||
### Fixes
|
### 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: 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.
|
- BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing.
|
||||||
- Web UI: hide internal `message_id` hints in chat bubbles.
|
- 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);
|
if (json) warnings.push(message);
|
||||||
else defaultRuntime.log(message);
|
else defaultRuntime.log(message);
|
||||||
},
|
},
|
||||||
configEnvVars: cfg.env?.vars,
|
config: cfg,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export async function maybeInstallDaemon(params: {
|
|||||||
token: params.gatewayToken,
|
token: params.gatewayToken,
|
||||||
runtime: daemonRuntime,
|
runtime: daemonRuntime,
|
||||||
warn: (message, title) => note(message, title),
|
warn: (message, title) => note(message, title),
|
||||||
configEnvVars: cfg.env?.vars,
|
config: cfg,
|
||||||
});
|
});
|
||||||
|
|
||||||
progress.setLabel("Installing Gateway service…");
|
progress.setLabel("Installing Gateway service…");
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ describe("buildGatewayInstallPlan", () => {
|
|||||||
expect(mocks.resolvePreferredNodePath).toHaveBeenCalled();
|
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.resolvePreferredNodePath.mockResolvedValue("/opt/node");
|
||||||
mocks.resolveGatewayProgramArguments.mockResolvedValue({
|
mocks.resolveGatewayProgramArguments.mockResolvedValue({
|
||||||
programArguments: ["node", "gateway"],
|
programArguments: ["node", "gateway"],
|
||||||
@@ -116,9 +116,13 @@ describe("buildGatewayInstallPlan", () => {
|
|||||||
env: {},
|
env: {},
|
||||||
port: 3000,
|
port: 3000,
|
||||||
runtime: "node",
|
runtime: "node",
|
||||||
configEnvVars: {
|
config: {
|
||||||
GOOGLE_API_KEY: "test-key",
|
env: {
|
||||||
CUSTOM_VAR: "custom-value",
|
vars: {
|
||||||
|
GOOGLE_API_KEY: "test-key",
|
||||||
|
},
|
||||||
|
CUSTOM_VAR: "custom-value",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -130,7 +134,7 @@ describe("buildGatewayInstallPlan", () => {
|
|||||||
expect(plan.environment.HOME).toBe("/Users/me");
|
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.resolvePreferredNodePath.mockResolvedValue("/opt/node");
|
||||||
mocks.resolveGatewayProgramArguments.mockResolvedValue({
|
mocks.resolveGatewayProgramArguments.mockResolvedValue({
|
||||||
programArguments: ["node", "gateway"],
|
programArguments: ["node", "gateway"],
|
||||||
@@ -147,16 +151,83 @@ describe("buildGatewayInstallPlan", () => {
|
|||||||
env: {},
|
env: {},
|
||||||
port: 3000,
|
port: 3000,
|
||||||
runtime: "node",
|
runtime: "node",
|
||||||
configEnvVars: {
|
config: {
|
||||||
VALID_KEY: "valid",
|
env: {
|
||||||
EMPTY_KEY: "",
|
vars: {
|
||||||
UNDEFINED_KEY: undefined,
|
VALID_KEY: "valid",
|
||||||
|
EMPTY_KEY: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(plan.environment.VALID_KEY).toBe("valid");
|
expect(plan.environment.VALID_KEY).toBe("valid");
|
||||||
expect(plan.environment.EMPTY_KEY).toBeUndefined();
|
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";
|
} from "../daemon/runtime-paths.js";
|
||||||
import { buildServiceEnvironment } from "../daemon/service-env.js";
|
import { buildServiceEnvironment } from "../daemon/service-env.js";
|
||||||
import { formatCliCommand } from "../cli/command-format.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";
|
import type { GatewayDaemonRuntime } from "./daemon-runtime.js";
|
||||||
|
|
||||||
type WarnFn = (message: string, title?: string) => void;
|
type WarnFn = (message: string, title?: string) => void;
|
||||||
@@ -31,8 +33,8 @@ export async function buildGatewayInstallPlan(params: {
|
|||||||
devMode?: boolean;
|
devMode?: boolean;
|
||||||
nodePath?: string;
|
nodePath?: string;
|
||||||
warn?: WarnFn;
|
warn?: WarnFn;
|
||||||
/** Environment variables from config.env.vars to include in the service environment */
|
/** Full config to extract env vars from (env vars + inline env keys). */
|
||||||
configEnvVars?: Record<string, string | undefined>;
|
config?: ClawdbotConfig;
|
||||||
}): Promise<GatewayInstallPlan> {
|
}): Promise<GatewayInstallPlan> {
|
||||||
const devMode = params.devMode ?? resolveGatewayDevMode();
|
const devMode = params.devMode ?? resolveGatewayDevMode();
|
||||||
const nodePath =
|
const nodePath =
|
||||||
@@ -62,14 +64,11 @@ export async function buildGatewayInstallPlan(params: {
|
|||||||
: undefined,
|
: 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.
|
// Config env vars are added first so service-specific vars take precedence.
|
||||||
const environment: Record<string, string | undefined> = {};
|
const environment: Record<string, string | undefined> = {
|
||||||
if (params.configEnvVars) {
|
...collectConfigEnvVars(params.config),
|
||||||
for (const [key, value] of Object.entries(params.configEnvVars)) {
|
};
|
||||||
if (value) environment[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Object.assign(environment, serviceEnvironment);
|
Object.assign(environment, serviceEnvironment);
|
||||||
|
|
||||||
return { programArguments, workingDirectory, environment };
|
return { programArguments, workingDirectory, environment };
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export async function maybeRepairGatewayDaemon(params: {
|
|||||||
token: params.cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN,
|
token: params.cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN,
|
||||||
runtime: daemonRuntime,
|
runtime: daemonRuntime,
|
||||||
warn: (message, title) => note(message, title),
|
warn: (message, title) => note(message, title),
|
||||||
configEnvVars: params.cfg.env?.vars,
|
config: params.cfg,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await service.install({
|
await service.install({
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export async function maybeMigrateLegacyGatewayService(
|
|||||||
token: cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN,
|
token: cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN,
|
||||||
runtime: daemonRuntime,
|
runtime: daemonRuntime,
|
||||||
warn: (message, title) => note(message, title),
|
warn: (message, title) => note(message, title),
|
||||||
configEnvVars: cfg.env?.vars,
|
config: cfg,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await service.install({
|
await service.install({
|
||||||
@@ -178,7 +178,7 @@ export async function maybeRepairGatewayServiceConfig(
|
|||||||
runtime: needsNodeRuntime && systemNodePath ? "node" : runtimeChoice,
|
runtime: needsNodeRuntime && systemNodePath ? "node" : runtimeChoice,
|
||||||
nodePath: systemNodePath ?? undefined,
|
nodePath: systemNodePath ?? undefined,
|
||||||
warn: (message, title) => note(message, title),
|
warn: (message, title) => note(message, title),
|
||||||
configEnvVars: cfg.env?.vars,
|
config: cfg,
|
||||||
});
|
});
|
||||||
const expectedEntrypoint = findGatewayEntrypoint(programArguments);
|
const expectedEntrypoint = findGatewayEntrypoint(programArguments);
|
||||||
const currentEntrypoint = findGatewayEntrypoint(command.programArguments);
|
const currentEntrypoint = findGatewayEntrypoint(command.programArguments);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export async function installGatewayDaemonNonInteractive(params: {
|
|||||||
token: gatewayToken,
|
token: gatewayToken,
|
||||||
runtime: daemonRuntimeRaw,
|
runtime: daemonRuntimeRaw,
|
||||||
warn: (message) => runtime.log(message),
|
warn: (message) => runtime.log(message),
|
||||||
configEnvVars: params.nextConfig.env?.vars,
|
config: params.nextConfig,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await service.install({
|
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";
|
} from "./defaults.js";
|
||||||
import { VERSION } from "../version.js";
|
import { VERSION } from "../version.js";
|
||||||
import { MissingEnvVarError, resolveConfigEnvVars } from "./env-substitution.js";
|
import { MissingEnvVarError, resolveConfigEnvVars } from "./env-substitution.js";
|
||||||
|
import { collectConfigEnvVars } from "./env-vars.js";
|
||||||
import { ConfigIncludeError, resolveConfigIncludes } from "./includes.js";
|
import { ConfigIncludeError, resolveConfigIncludes } from "./includes.js";
|
||||||
import { findLegacyConfigIssues } from "./legacy.js";
|
import { findLegacyConfigIssues } from "./legacy.js";
|
||||||
import { normalizeConfigPaths } from "./normalize-paths.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 {
|
function applyConfigEnv(cfg: ClawdbotConfig, env: NodeJS.ProcessEnv): void {
|
||||||
const envConfig = cfg.env;
|
const entries = collectConfigEnvVars(cfg);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(entries)) {
|
for (const [key, value] of Object.entries(entries)) {
|
||||||
if (env[key]?.trim()) continue;
|
if (env[key]?.trim()) continue;
|
||||||
env[key] = value;
|
env[key] = value;
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
|
|||||||
token: settings.gatewayToken,
|
token: settings.gatewayToken,
|
||||||
runtime: daemonRuntime,
|
runtime: daemonRuntime,
|
||||||
warn: (message, title) => prompter.note(message, title),
|
warn: (message, title) => prompter.note(message, title),
|
||||||
configEnvVars: nextConfig.env?.vars,
|
config: nextConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
progress.update("Installing Gateway service…");
|
progress.update("Installing Gateway service…");
|
||||||
|
|||||||
Reference in New Issue
Block a user