Skip to content

Commit 02cf129

Browse files
la14-1louisgvclaude
authored
fix(spawn-fix): load API keys via config file, not just process.env (#3095)
Previously buildFixScript() resolved env templates directly from process.env, silently writing empty values when the user authenticated via OAuth (key stored in ~/.config/spawn/openrouter.json). Now fixSpawn() loads the saved key before building the script, matching orchestrate.ts. Fixes #3094 Agent: code-health Co-authored-by: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 52e78bd commit 02cf129

4 files changed

Lines changed: 62 additions & 2 deletions

File tree

packages/cli/src/__tests__/cmd-fix-cov.test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import type { SpawnRecord } from "../history";
1212

13-
import { beforeEach, describe, expect, it, mock } from "bun:test";
13+
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
1414
import { tryCatch } from "@openrouter/spawn-shared";
1515
import { createMockManifest, mockClackPrompts } from "./test-helpers";
1616

@@ -51,13 +51,25 @@ function makeRecord(overrides: Partial<SpawnRecord> = {}): SpawnRecord {
5151
// ── Tests: fixSpawn edge cases ──────────────────────────────────────────────
5252

5353
describe("fixSpawn (additional coverage)", () => {
54+
let savedApiKey: string | undefined;
55+
5456
beforeEach(() => {
57+
savedApiKey = process.env.OPENROUTER_API_KEY;
58+
process.env.OPENROUTER_API_KEY = "sk-or-test-fix-key";
5559
clack.logError.mockReset();
5660
clack.logInfo.mockReset();
5761
clack.logSuccess.mockReset();
5862
clack.logStep.mockReset();
5963
});
6064

65+
afterEach(() => {
66+
if (savedApiKey === undefined) {
67+
delete process.env.OPENROUTER_API_KEY;
68+
} else {
69+
process.env.OPENROUTER_API_KEY = savedApiKey;
70+
}
71+
});
72+
6173
it("shows error for invalid server_name in connection", async () => {
6274
const record = makeRecord({
6375
connection: {
@@ -145,12 +157,24 @@ describe("fixSpawn (additional coverage)", () => {
145157
// (error paths are covered in cmd-fix.test.ts; this covers the exact success message)
146158

147159
describe("fixSpawn connection edge cases", () => {
160+
let savedApiKey: string | undefined;
161+
148162
beforeEach(() => {
163+
savedApiKey = process.env.OPENROUTER_API_KEY;
164+
process.env.OPENROUTER_API_KEY = "sk-or-test-fix-key";
149165
clack.logError.mockReset();
150166
clack.logSuccess.mockReset();
151167
clack.logStep.mockReset();
152168
});
153169

170+
afterEach(() => {
171+
if (savedApiKey === undefined) {
172+
delete process.env.OPENROUTER_API_KEY;
173+
} else {
174+
process.env.OPENROUTER_API_KEY = savedApiKey;
175+
}
176+
});
177+
154178
it("shows success when fix script succeeds", async () => {
155179
const mockRunner = mock(async () => true);
156180
const record = makeRecord();

packages/cli/src/__tests__/cmd-fix.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,13 +194,25 @@ describe("buildFixScript", () => {
194194
// ── Tests: fixSpawn (DI for SSH runner) ─────────────────────────────────────
195195

196196
describe("fixSpawn", () => {
197+
let savedApiKey: string | undefined;
198+
197199
beforeEach(() => {
200+
savedApiKey = process.env.OPENROUTER_API_KEY;
201+
process.env.OPENROUTER_API_KEY = "sk-or-test-fix-key";
198202
clack.logError.mockReset();
199203
clack.logSuccess.mockReset();
200204
clack.logInfo.mockReset();
201205
clack.logStep.mockReset();
202206
});
203207

208+
afterEach(() => {
209+
if (savedApiKey === undefined) {
210+
delete process.env.OPENROUTER_API_KEY;
211+
} else {
212+
process.env.OPENROUTER_API_KEY = savedApiKey;
213+
}
214+
});
215+
204216
it("shows error for record without connection info", async () => {
205217
const record = makeRecord({
206218
connection: undefined,
@@ -309,6 +321,7 @@ describe("fixSpawn", () => {
309321
describe("cmdFix", () => {
310322
let testDir: string;
311323
let savedSpawnHome: string | undefined;
324+
let savedApiKey: string | undefined;
312325
let processExitSpy: ReturnType<typeof spyOn>;
313326

314327
function writeHistory(records: SpawnRecord[]) {
@@ -328,6 +341,8 @@ describe("cmdFix", () => {
328341
});
329342
savedSpawnHome = process.env.SPAWN_HOME;
330343
process.env.SPAWN_HOME = testDir;
344+
savedApiKey = process.env.OPENROUTER_API_KEY;
345+
process.env.OPENROUTER_API_KEY = "sk-or-test-fix-key";
331346
clack.logError.mockReset();
332347
clack.logSuccess.mockReset();
333348
clack.logInfo.mockReset();
@@ -338,6 +353,11 @@ describe("cmdFix", () => {
338353

339354
afterEach(() => {
340355
process.env.SPAWN_HOME = savedSpawnHome;
356+
if (savedApiKey === undefined) {
357+
delete process.env.OPENROUTER_API_KEY;
358+
} else {
359+
process.env.OPENROUTER_API_KEY = savedApiKey;
360+
}
341361
processExitSpy.mockRestore();
342362
if (existsSync(testDir)) {
343363
rmSync(testDir, {

packages/cli/src/commands/fix.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import pc from "picocolors";
88
import { getActiveServers } from "../history.js";
99
import { loadManifest } from "../manifest.js";
1010
import { validateConnectionIP, validateIdentifier, validateServerIdentifier, validateUsername } from "../security.js";
11+
import { loadSavedOpenRouterKey } from "../shared/oauth.js";
1112
import { getHistoryPath } from "../shared/paths.js";
1213
import { asyncTryCatch, tryCatch } from "../shared/result.js";
1314
import { SSH_INTERACTIVE_OPTS } from "../shared/ssh.js";
@@ -176,6 +177,21 @@ export async function fixSpawn(record: SpawnRecord, manifest: Manifest | null, o
176177
return;
177178
}
178179

180+
// Ensure OPENROUTER_API_KEY is available before building the fix script.
181+
// The normal provisioning flow uses getOrPromptApiKey() which loads from
182+
// ~/.config/spawn/openrouter.json. buildFixScript() resolves env templates
183+
// from process.env, so we must populate it here to avoid injecting empty keys.
184+
if (!process.env.OPENROUTER_API_KEY) {
185+
const savedKey = loadSavedOpenRouterKey();
186+
if (savedKey) {
187+
process.env.OPENROUTER_API_KEY = savedKey;
188+
} else {
189+
p.log.error("No OpenRouter API key found.");
190+
p.log.info("Set OPENROUTER_API_KEY in your environment, or run a new spawn to authenticate via OAuth.");
191+
return;
192+
}
193+
}
194+
179195
// Build the remote fix script
180196
const scriptResult = tryCatch(() => buildFixScript(man!, record.agent));
181197
if (!scriptResult.ok) {

packages/cli/src/shared/oauth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ export function hasSavedOpenRouterKey(): boolean {
285285
}
286286

287287
/** Load a previously saved OpenRouter API key from ~/.config/spawn/openrouter.json. */
288-
function loadSavedOpenRouterKey(): string | null {
288+
export function loadSavedOpenRouterKey(): string | null {
289289
const result = tryCatch(() => {
290290
const configPath = getSpawnCloudConfigPath("openrouter");
291291
const data = parseJsonObj(readFileSync(configPath, "utf-8"));

0 commit comments

Comments
 (0)