From ba7d12f2051d702f6f5b30bc8d2584aaaf8d378a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 13 Jan 2026 07:12:17 +0000 Subject: [PATCH] refactor: centralize onboarding auth paths --- src/agents/agent-scope.ts | 9 ---- src/commands/auth-choice.test.ts | 34 ++++---------- src/commands/onboard-auth.test.ts | 76 +++++++++++++++++++++++++++---- src/commands/onboard-auth.ts | 37 ++++++++------- 4 files changed, 96 insertions(+), 60 deletions(-) diff --git a/src/agents/agent-scope.ts b/src/agents/agent-scope.ts index fa32a5fd1..718cf877d 100644 --- a/src/agents/agent-scope.ts +++ b/src/agents/agent-scope.ts @@ -155,12 +155,3 @@ export function resolveAgentDir(cfg: ClawdbotConfig, agentId: string) { const root = resolveStateDir(process.env, os.homedir); return path.join(root, "agents", id, "agent"); } - -/** - * Resolve the agent directory for the default agent without requiring config. - * Used by onboarding when writing auth profiles before config is fully set up. - */ -export function resolveDefaultAgentDir(): string { - const root = resolveStateDir(process.env, os.homedir); - return path.join(root, "agents", DEFAULT_AGENT_ID, "agent"); -} diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.test.ts index 441d954bc..bd3f6cf29 100644 --- a/src/commands/auth-choice.test.ts +++ b/src/commands/auth-choice.test.ts @@ -18,6 +18,8 @@ vi.mock("../providers/github-copilot-auth.js", () => ({ const noopAsync = async () => {}; const noop = () => {}; +const authProfilePathFor = (agentDir: string) => + path.join(agentDir, "auth-profiles.json"); describe("applyAuthChoice", () => { const previousStateDir = process.env.CLAWDBOT_STATE_DIR; @@ -111,12 +113,8 @@ describe("applyAuthChoice", () => { mode: "api_key", }); - const authProfilePath = path.join( - tempStateDir, - "agents", - "main", - "agent", - "auth-profiles.json", + const authProfilePath = authProfilePathFor( + process.env.CLAWDBOT_AGENT_DIR!, ); const raw = await fs.readFile(authProfilePath, "utf8"); const parsed = JSON.parse(raw) as { @@ -170,12 +168,8 @@ describe("applyAuthChoice", () => { mode: "api_key", }); - const authProfilePath = path.join( - tempStateDir, - "agents", - "main", - "agent", - "auth-profiles.json", + const authProfilePath = authProfilePathFor( + process.env.CLAWDBOT_AGENT_DIR!, ); const raw = await fs.readFile(authProfilePath, "utf8"); const parsed = JSON.parse(raw) as { @@ -337,12 +331,8 @@ describe("applyAuthChoice", () => { "openrouter/auto", ); - const authProfilePath = path.join( - tempStateDir, - "agents", - "main", - "agent", - "auth-profiles.json", + const authProfilePath = authProfilePathFor( + process.env.CLAWDBOT_AGENT_DIR!, ); const raw = await fs.readFile(authProfilePath, "utf8"); const parsed = JSON.parse(raw) as { @@ -426,12 +416,8 @@ describe("applyAuthChoice", () => { mode: "oauth", }); - const authProfilePath = path.join( - tempStateDir, - "agents", - "main", - "agent", - "auth-profiles.json", + const authProfilePath = authProfilePathFor( + process.env.CLAWDBOT_AGENT_DIR!, ); const raw = await fs.readFile(authProfilePath, "utf8"); const parsed = JSON.parse(raw) as { diff --git a/src/commands/onboard-auth.test.ts b/src/commands/onboard-auth.test.ts index ba91697f2..158011349 100644 --- a/src/commands/onboard-auth.test.ts +++ b/src/commands/onboard-auth.test.ts @@ -16,11 +16,15 @@ import { applySyntheticConfig, applySyntheticProviderConfig, OPENROUTER_DEFAULT_MODEL_REF, + setMinimaxApiKey, SYNTHETIC_DEFAULT_MODEL_ID, SYNTHETIC_DEFAULT_MODEL_REF, writeOAuthCredentials, } from "./onboard-auth.js"; +const authProfilePathFor = (agentDir: string) => + path.join(agentDir, "auth-profiles.json"); + describe("writeOAuthCredentials", () => { const previousStateDir = process.env.CLAWDBOT_STATE_DIR; const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR; @@ -50,10 +54,9 @@ describe("writeOAuthCredentials", () => { delete process.env.CLAWDBOT_OAUTH_DIR; }); - it("writes auth-profiles.json under CLAWDBOT_STATE_DIR/agents/main/agent", async () => { + it("writes auth-profiles.json under CLAWDBOT_AGENT_DIR when set", async () => { tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-oauth-")); process.env.CLAWDBOT_STATE_DIR = tempStateDir; - // Even if legacy env vars are set, onboarding should write to the multi-agent path. process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent"); process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR; @@ -65,13 +68,8 @@ describe("writeOAuthCredentials", () => { await writeOAuthCredentials("openai-codex", creds); - // Now writes to the multi-agent path: agents/main/agent - const authProfilePath = path.join( - tempStateDir, - "agents", - "main", - "agent", - "auth-profiles.json", + const authProfilePath = authProfilePathFor( + process.env.CLAWDBOT_AGENT_DIR!, ); const raw = await fs.readFile(authProfilePath, "utf8"); const parsed = JSON.parse(raw) as { @@ -85,7 +83,65 @@ describe("writeOAuthCredentials", () => { await expect( fs.readFile( - path.join(tempStateDir, "agent", "auth-profiles.json"), + path.join(tempStateDir, "agents", "main", "agent", "auth-profiles.json"), + "utf8", + ), + ).rejects.toThrow(); + }); +}); + +describe("setMinimaxApiKey", () => { + const previousStateDir = process.env.CLAWDBOT_STATE_DIR; + const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR; + const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR; + let tempStateDir: string | null = null; + + afterEach(async () => { + if (tempStateDir) { + await fs.rm(tempStateDir, { recursive: true, force: true }); + tempStateDir = null; + } + if (previousStateDir === undefined) { + delete process.env.CLAWDBOT_STATE_DIR; + } else { + process.env.CLAWDBOT_STATE_DIR = previousStateDir; + } + if (previousAgentDir === undefined) { + delete process.env.CLAWDBOT_AGENT_DIR; + } else { + process.env.CLAWDBOT_AGENT_DIR = previousAgentDir; + } + if (previousPiAgentDir === undefined) { + delete process.env.PI_CODING_AGENT_DIR; + } else { + process.env.PI_CODING_AGENT_DIR = previousPiAgentDir; + } + }); + + it("writes to CLAWDBOT_AGENT_DIR when set", async () => { + tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-minimax-")); + process.env.CLAWDBOT_STATE_DIR = tempStateDir; + process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "custom-agent"); + process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR; + + await setMinimaxApiKey("sk-minimax-test"); + + const customAuthPath = authProfilePathFor( + process.env.CLAWDBOT_AGENT_DIR!, + ); + const raw = await fs.readFile(customAuthPath, "utf8"); + const parsed = JSON.parse(raw) as { + profiles?: Record; + }; + expect(parsed.profiles?.["minimax:default"]).toMatchObject({ + type: "api_key", + provider: "minimax", + key: "sk-minimax-test", + }); + + await expect( + fs.readFile( + path.join(tempStateDir, "agents", "main", "agent", "auth-profiles.json"), "utf8", ), ).rejects.toThrow(); diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index 39664fec4..4e7e0f790 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -1,5 +1,5 @@ import type { OAuthCredentials } from "@mariozechner/pi-ai"; -import { resolveDefaultAgentDir } from "../agents/agent-scope.js"; +import { resolveClawdbotAgentDir } from "../agents/agent-paths.js"; import { upsertAuthProfile } from "../agents/auth-profiles.js"; import { OPENCODE_ZEN_DEFAULT_MODEL_REF } from "../agents/opencode-zen-models.js"; import { @@ -24,6 +24,9 @@ const MOONSHOT_DEFAULT_CONTEXT_WINDOW = 256000; const MOONSHOT_DEFAULT_MAX_TOKENS = 8192; export const MOONSHOT_DEFAULT_MODEL_REF = `moonshot/${MOONSHOT_DEFAULT_MODEL_ID}`; export { SYNTHETIC_DEFAULT_MODEL_ID, SYNTHETIC_DEFAULT_MODEL_REF }; + +const resolveAuthAgentDir = (agentDir?: string) => + agentDir ?? resolveClawdbotAgentDir(); // Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs. const MINIMAX_API_COST = { input: 15, @@ -107,7 +110,7 @@ export async function writeOAuthCredentials( creds: OAuthCredentials, agentDir?: string, ): Promise { - // Write to the multi-agent path so gateway finds credentials on startup + // Write to resolved agent dir so gateway finds credentials on startup. upsertAuthProfile({ profileId: `${provider}:${creds.email ?? "default"}`, credential: { @@ -115,12 +118,12 @@ export async function writeOAuthCredentials( provider, ...creds, }, - agentDir: agentDir ?? resolveDefaultAgentDir(), + agentDir: resolveAuthAgentDir(agentDir), }); } export async function setAnthropicApiKey(key: string, agentDir?: string) { - // Write to the multi-agent path so gateway finds credentials on startup + // Write to resolved agent dir so gateway finds credentials on startup. upsertAuthProfile({ profileId: "anthropic:default", credential: { @@ -128,12 +131,12 @@ export async function setAnthropicApiKey(key: string, agentDir?: string) { provider: "anthropic", key, }, - agentDir: agentDir ?? resolveDefaultAgentDir(), + agentDir: resolveAuthAgentDir(agentDir), }); } export async function setGeminiApiKey(key: string, agentDir?: string) { - // Write to the multi-agent path so gateway finds credentials on startup + // Write to resolved agent dir so gateway finds credentials on startup. upsertAuthProfile({ profileId: "google:default", credential: { @@ -141,12 +144,12 @@ export async function setGeminiApiKey(key: string, agentDir?: string) { provider: "google", key, }, - agentDir: agentDir ?? resolveDefaultAgentDir(), + agentDir: resolveAuthAgentDir(agentDir), }); } export async function setMinimaxApiKey(key: string, agentDir?: string) { - // Write to the multi-agent path so gateway finds credentials on startup + // Write to resolved agent dir so gateway finds credentials on startup. upsertAuthProfile({ profileId: "minimax:default", credential: { @@ -154,12 +157,12 @@ export async function setMinimaxApiKey(key: string, agentDir?: string) { provider: "minimax", key, }, - agentDir: agentDir ?? resolveDefaultAgentDir(), + agentDir: resolveAuthAgentDir(agentDir), }); } export async function setMoonshotApiKey(key: string, agentDir?: string) { - // Write to the multi-agent path so gateway finds credentials on startup + // Write to resolved agent dir so gateway finds credentials on startup. upsertAuthProfile({ profileId: "moonshot:default", credential: { @@ -167,12 +170,12 @@ export async function setMoonshotApiKey(key: string, agentDir?: string) { provider: "moonshot", key, }, - agentDir: agentDir ?? resolveDefaultAgentDir(), + agentDir: resolveAuthAgentDir(agentDir), }); } export async function setSyntheticApiKey(key: string, agentDir?: string) { - // Write to the multi-agent path so gateway finds credentials on startup + // Write to resolved agent dir so gateway finds credentials on startup. upsertAuthProfile({ profileId: "synthetic:default", credential: { @@ -180,7 +183,7 @@ export async function setSyntheticApiKey(key: string, agentDir?: string) { provider: "synthetic", key, }, - agentDir: agentDir ?? resolveDefaultAgentDir(), + agentDir: resolveAuthAgentDir(agentDir), }); } @@ -188,7 +191,7 @@ export const ZAI_DEFAULT_MODEL_REF = "zai/glm-4.7"; export const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto"; export async function setZaiApiKey(key: string, agentDir?: string) { - // Write to the multi-agent path so gateway finds credentials on startup + // Write to resolved agent dir so gateway finds credentials on startup. upsertAuthProfile({ profileId: "zai:default", credential: { @@ -196,7 +199,7 @@ export async function setZaiApiKey(key: string, agentDir?: string) { provider: "zai", key, }, - agentDir: agentDir ?? resolveDefaultAgentDir(), + agentDir: resolveAuthAgentDir(agentDir), }); } @@ -208,7 +211,7 @@ export async function setOpenrouterApiKey(key: string, agentDir?: string) { provider: "openrouter", key, }, - agentDir: agentDir ?? resolveDefaultAgentDir(), + agentDir: resolveAuthAgentDir(agentDir), }); } @@ -714,7 +717,7 @@ export async function setOpencodeZenApiKey(key: string, agentDir?: string) { provider: "opencode", key, }, - agentDir: agentDir ?? resolveDefaultAgentDir(), + agentDir: resolveAuthAgentDir(agentDir), }); }