fix: load tool API keys from auth.json at session startup (#563)

Export TOOL_KEYS constant and add loadToolApiKeys() function to load
API keys from ~/.gsd/agent/auth.json into environment variables.

Called in session_start handler so tool-based extensions (Context7,
Brave Search, Jina, Tavily, Groq) work immediately without requiring
/gsd config.
This commit is contained in:
geromet 2026-03-16 01:58:00 +01:00 committed by GitHub
parent c20c57b941
commit dfd2a1b5b4
2 changed files with 32 additions and 3 deletions

View file

@ -628,7 +628,12 @@ function serializePreferencesToFrontmatter(prefs: Record<string, unknown>): stri
// ─── Tool Config Wizard ───────────────────────────────────────────────────────
const TOOL_KEYS = [
/**
* Tool API key configurations.
* This is the source of truth for tool credentials - used by both the config wizard
* and session startup to load keys from auth.json into environment variables.
*/
export const TOOL_KEYS = [
{ id: "tavily", env: "TAVILY_API_KEY", label: "Tavily Search", hint: "tavily.com/app/api-keys" },
{ id: "brave", env: "BRAVE_API_KEY", label: "Brave Search", hint: "brave.com/search/api" },
{ id: "context7", env: "CONTEXT7_API_KEY", label: "Context7 Docs", hint: "context7.com/dashboard" },
@ -636,6 +641,27 @@ const TOOL_KEYS = [
{ id: "groq", env: "GROQ_API_KEY", label: "Groq Voice", hint: "console.groq.com" },
] as const;
/**
* Load tool API keys from auth.json into environment variables.
* Called at session startup to ensure tools have access to their credentials.
*/
export function loadToolApiKeys(): void {
try {
const authPath = join(process.env.HOME ?? "", ".gsd", "agent", "auth.json");
if (!existsSync(authPath)) return;
const auth = AuthStorage.create(authPath);
for (const tool of TOOL_KEYS) {
const cred = auth.get(tool.id);
if (cred && "key" in cred && cred.key && !process.env[tool.env]) {
process.env[tool.env] = cred.key;
}
}
} catch {
// Failed to load tool keys — ignore, they can still be set via env vars
}
}
function getConfigAuthStorage(): InstanceType<typeof AuthStorage> {
const authPath = join(process.env.HOME ?? "", ".gsd", "agent", "auth.json");
mkdirSync(dirname(authPath), { recursive: true });

View file

@ -25,7 +25,7 @@ import type {
} from "@gsd/pi-coding-agent";
import { createBashTool, createWriteTool, createReadTool, createEditTool, isToolCallEventType } from "@gsd/pi-coding-agent";
import { registerGSDCommand } from "./commands.js";
import { registerGSDCommand, loadToolApiKeys } from "./commands.js";
import { registerExitCommand } from "./exit-command.js";
import { registerWorktreeCommand, getWorktreeOriginalCwd, getActiveWorktreeName } from "./worktree-command.js";
import { saveFile, formatContinue, loadFile, parseContinue, parseSummary } from "./files.js";
@ -188,7 +188,7 @@ export default function (pi: ExtensionAPI) {
};
pi.registerTool(dynamicEdit as any);
// ── session_start: render branded GSD header + remote channel status ──
// ── session_start: render branded GSD header + load tool keys + remote status ──
pi.on("session_start", async (_event, ctx) => {
// Theme access throws in RPC mode (no TUI) — header is decorative, skip it
try {
@ -204,6 +204,9 @@ export default function (pi: ExtensionAPI) {
// RPC mode — no TUI, skip header rendering
}
// Load tool API keys from auth.json into environment
loadToolApiKeys();
// Notify remote questions status if configured
try {
const [{ getRemoteConfigStatus }, { getLatestPromptSummary }] = await Promise.all([