From 114ff23f2ada6d9a7af89c7cbfa0bfb628b5e68d Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 13 Apr 2026 19:09:02 +0100 Subject: [PATCH] perf(config): skip shell env fallback for explicit empty vars --- src/cli/daemon-cli/install.test.ts | 9 +++++++++ src/infra/shell-env.test.ts | 18 ++++++++++++++++++ src/infra/shell-env.ts | 8 ++++++-- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/cli/daemon-cli/install.test.ts b/src/cli/daemon-cli/install.test.ts index 55604952c54..dad1497b0d9 100644 --- a/src/cli/daemon-cli/install.test.ts +++ b/src/cli/daemon-cli/install.test.ts @@ -12,6 +12,14 @@ const resolveIsNixModeMock = vi.hoisted(() => vi.fn(() => false)); const resolveSecretInputRefMock = vi.hoisted(() => vi.fn((): { ref: unknown } => ({ ref: undefined })), ); +const hasConfiguredSecretInputMock = vi.hoisted(() => + vi.fn((value: unknown): boolean => { + if (typeof value === "string" && value.trim()) { + return true; + } + return resolveSecretInputRefMock(value as never)?.ref != null; + }), +); const resolveGatewayAuthMock = vi.hoisted(() => vi.fn(() => ({ mode: "token", @@ -70,6 +78,7 @@ vi.mock("../../config/paths.js", () => ({ })); vi.mock("../../config/types.secrets.js", () => ({ + hasConfiguredSecretInput: hasConfiguredSecretInputMock, resolveSecretInputRef: resolveSecretInputRefMock, })); diff --git a/src/infra/shell-env.test.ts b/src/infra/shell-env.test.ts index 973f2b14630..11f5e6db6b0 100644 --- a/src/infra/shell-env.test.ts +++ b/src/infra/shell-env.test.ts @@ -154,6 +154,24 @@ describe("shell env fallback", () => { expect(exec).not.toHaveBeenCalled(); }); + it("treats explicitly empty env vars as intentional overrides", () => { + const env: NodeJS.ProcessEnv = { OPENAI_API_KEY: "" }; + const exec = vi.fn(() => Buffer.from("OPENAI_API_KEY=from-shell\0")); + + const res = runShellEnvFallback({ + enabled: true, + env, + expectedKeys: ["OPENAI_API_KEY"], + exec, + }); + + expect(res.ok).toBe(true); + expect(res.applied).toEqual([]); + expect(res.ok && res.skippedReason).toBe("already-has-keys"); + expect(env.OPENAI_API_KEY).toBe(""); + expect(exec).not.toHaveBeenCalled(); + }); + it("imports expected keys without overriding existing env", () => { const env: NodeJS.ProcessEnv = {}; const exec = vi.fn(() => Buffer.from("OPENAI_API_KEY=from-shell\0DISCORD_BOT_TOKEN=discord\0")); diff --git a/src/infra/shell-env.ts b/src/infra/shell-env.ts index db9dbf47948..d0614a5349d 100644 --- a/src/infra/shell-env.ts +++ b/src/infra/shell-env.ts @@ -208,6 +208,10 @@ export type ShellEnvFallbackOptions = { exec?: typeof execFileSync; }; +function hasExplicitEnvBinding(env: NodeJS.ProcessEnv, key: string): boolean { + return Object.prototype.hasOwnProperty.call(env, key); +} + export function loadShellEnvFallback(opts: ShellEnvFallbackOptions): ShellEnvFallbackResult { const logger = opts.logger ?? console; @@ -216,7 +220,7 @@ export function loadShellEnvFallback(opts: ShellEnvFallbackOptions): ShellEnvFal return { ok: true, applied: [], skippedReason: "disabled" }; } - const hasAnyKey = opts.expectedKeys.some((key) => Boolean(opts.env[key]?.trim())); + const hasAnyKey = opts.expectedKeys.some((key) => hasExplicitEnvBinding(opts.env, key)); if (hasAnyKey) { lastAppliedKeys = []; return { ok: true, applied: [], skippedReason: "already-has-keys" }; @@ -235,7 +239,7 @@ export function loadShellEnvFallback(opts: ShellEnvFallbackOptions): ShellEnvFal const applied: string[] = []; for (const key of opts.expectedKeys) { - if (opts.env[key]?.trim()) { + if (hasExplicitEnvBinding(opts.env, key)) { continue; } const value = probe.shellEnv.get(key);