refactor: centralize onboarding auth paths

This commit is contained in:
Peter Steinberger
2026-01-13 07:12:17 +00:00
parent ef66ad3b52
commit ba7d12f205
4 changed files with 96 additions and 60 deletions

View File

@@ -155,12 +155,3 @@ export function resolveAgentDir(cfg: ClawdbotConfig, agentId: string) {
const root = resolveStateDir(process.env, os.homedir); const root = resolveStateDir(process.env, os.homedir);
return path.join(root, "agents", id, "agent"); 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");
}

View File

@@ -18,6 +18,8 @@ vi.mock("../providers/github-copilot-auth.js", () => ({
const noopAsync = async () => {}; const noopAsync = async () => {};
const noop = () => {}; const noop = () => {};
const authProfilePathFor = (agentDir: string) =>
path.join(agentDir, "auth-profiles.json");
describe("applyAuthChoice", () => { describe("applyAuthChoice", () => {
const previousStateDir = process.env.CLAWDBOT_STATE_DIR; const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
@@ -111,12 +113,8 @@ describe("applyAuthChoice", () => {
mode: "api_key", mode: "api_key",
}); });
const authProfilePath = path.join( const authProfilePath = authProfilePathFor(
tempStateDir, process.env.CLAWDBOT_AGENT_DIR!,
"agents",
"main",
"agent",
"auth-profiles.json",
); );
const raw = await fs.readFile(authProfilePath, "utf8"); const raw = await fs.readFile(authProfilePath, "utf8");
const parsed = JSON.parse(raw) as { const parsed = JSON.parse(raw) as {
@@ -170,12 +168,8 @@ describe("applyAuthChoice", () => {
mode: "api_key", mode: "api_key",
}); });
const authProfilePath = path.join( const authProfilePath = authProfilePathFor(
tempStateDir, process.env.CLAWDBOT_AGENT_DIR!,
"agents",
"main",
"agent",
"auth-profiles.json",
); );
const raw = await fs.readFile(authProfilePath, "utf8"); const raw = await fs.readFile(authProfilePath, "utf8");
const parsed = JSON.parse(raw) as { const parsed = JSON.parse(raw) as {
@@ -337,12 +331,8 @@ describe("applyAuthChoice", () => {
"openrouter/auto", "openrouter/auto",
); );
const authProfilePath = path.join( const authProfilePath = authProfilePathFor(
tempStateDir, process.env.CLAWDBOT_AGENT_DIR!,
"agents",
"main",
"agent",
"auth-profiles.json",
); );
const raw = await fs.readFile(authProfilePath, "utf8"); const raw = await fs.readFile(authProfilePath, "utf8");
const parsed = JSON.parse(raw) as { const parsed = JSON.parse(raw) as {
@@ -426,12 +416,8 @@ describe("applyAuthChoice", () => {
mode: "oauth", mode: "oauth",
}); });
const authProfilePath = path.join( const authProfilePath = authProfilePathFor(
tempStateDir, process.env.CLAWDBOT_AGENT_DIR!,
"agents",
"main",
"agent",
"auth-profiles.json",
); );
const raw = await fs.readFile(authProfilePath, "utf8"); const raw = await fs.readFile(authProfilePath, "utf8");
const parsed = JSON.parse(raw) as { const parsed = JSON.parse(raw) as {

View File

@@ -16,11 +16,15 @@ import {
applySyntheticConfig, applySyntheticConfig,
applySyntheticProviderConfig, applySyntheticProviderConfig,
OPENROUTER_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF,
setMinimaxApiKey,
SYNTHETIC_DEFAULT_MODEL_ID, SYNTHETIC_DEFAULT_MODEL_ID,
SYNTHETIC_DEFAULT_MODEL_REF, SYNTHETIC_DEFAULT_MODEL_REF,
writeOAuthCredentials, writeOAuthCredentials,
} from "./onboard-auth.js"; } from "./onboard-auth.js";
const authProfilePathFor = (agentDir: string) =>
path.join(agentDir, "auth-profiles.json");
describe("writeOAuthCredentials", () => { describe("writeOAuthCredentials", () => {
const previousStateDir = process.env.CLAWDBOT_STATE_DIR; const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR; const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR;
@@ -50,10 +54,9 @@ describe("writeOAuthCredentials", () => {
delete process.env.CLAWDBOT_OAUTH_DIR; 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-")); tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-oauth-"));
process.env.CLAWDBOT_STATE_DIR = tempStateDir; 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.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent");
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR; process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
@@ -65,13 +68,8 @@ describe("writeOAuthCredentials", () => {
await writeOAuthCredentials("openai-codex", creds); await writeOAuthCredentials("openai-codex", creds);
// Now writes to the multi-agent path: agents/main/agent const authProfilePath = authProfilePathFor(
const authProfilePath = path.join( process.env.CLAWDBOT_AGENT_DIR!,
tempStateDir,
"agents",
"main",
"agent",
"auth-profiles.json",
); );
const raw = await fs.readFile(authProfilePath, "utf8"); const raw = await fs.readFile(authProfilePath, "utf8");
const parsed = JSON.parse(raw) as { const parsed = JSON.parse(raw) as {
@@ -85,7 +83,65 @@ describe("writeOAuthCredentials", () => {
await expect( await expect(
fs.readFile( 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<string, { type?: string; provider?: string; key?: string }>;
};
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", "utf8",
), ),
).rejects.toThrow(); ).rejects.toThrow();

View File

@@ -1,5 +1,5 @@
import type { OAuthCredentials } from "@mariozechner/pi-ai"; 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 { upsertAuthProfile } from "../agents/auth-profiles.js";
import { OPENCODE_ZEN_DEFAULT_MODEL_REF } from "../agents/opencode-zen-models.js"; import { OPENCODE_ZEN_DEFAULT_MODEL_REF } from "../agents/opencode-zen-models.js";
import { import {
@@ -24,6 +24,9 @@ const MOONSHOT_DEFAULT_CONTEXT_WINDOW = 256000;
const MOONSHOT_DEFAULT_MAX_TOKENS = 8192; const MOONSHOT_DEFAULT_MAX_TOKENS = 8192;
export const MOONSHOT_DEFAULT_MODEL_REF = `moonshot/${MOONSHOT_DEFAULT_MODEL_ID}`; export const MOONSHOT_DEFAULT_MODEL_REF = `moonshot/${MOONSHOT_DEFAULT_MODEL_ID}`;
export { SYNTHETIC_DEFAULT_MODEL_ID, SYNTHETIC_DEFAULT_MODEL_REF }; 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. // Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs.
const MINIMAX_API_COST = { const MINIMAX_API_COST = {
input: 15, input: 15,
@@ -107,7 +110,7 @@ export async function writeOAuthCredentials(
creds: OAuthCredentials, creds: OAuthCredentials,
agentDir?: string, agentDir?: string,
): Promise<void> { ): Promise<void> {
// Write to the multi-agent path so gateway finds credentials on startup // Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({ upsertAuthProfile({
profileId: `${provider}:${creds.email ?? "default"}`, profileId: `${provider}:${creds.email ?? "default"}`,
credential: { credential: {
@@ -115,12 +118,12 @@ export async function writeOAuthCredentials(
provider, provider,
...creds, ...creds,
}, },
agentDir: agentDir ?? resolveDefaultAgentDir(), agentDir: resolveAuthAgentDir(agentDir),
}); });
} }
export async function setAnthropicApiKey(key: string, agentDir?: string) { 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({ upsertAuthProfile({
profileId: "anthropic:default", profileId: "anthropic:default",
credential: { credential: {
@@ -128,12 +131,12 @@ export async function setAnthropicApiKey(key: string, agentDir?: string) {
provider: "anthropic", provider: "anthropic",
key, key,
}, },
agentDir: agentDir ?? resolveDefaultAgentDir(), agentDir: resolveAuthAgentDir(agentDir),
}); });
} }
export async function setGeminiApiKey(key: string, agentDir?: string) { 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({ upsertAuthProfile({
profileId: "google:default", profileId: "google:default",
credential: { credential: {
@@ -141,12 +144,12 @@ export async function setGeminiApiKey(key: string, agentDir?: string) {
provider: "google", provider: "google",
key, key,
}, },
agentDir: agentDir ?? resolveDefaultAgentDir(), agentDir: resolveAuthAgentDir(agentDir),
}); });
} }
export async function setMinimaxApiKey(key: string, agentDir?: string) { 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({ upsertAuthProfile({
profileId: "minimax:default", profileId: "minimax:default",
credential: { credential: {
@@ -154,12 +157,12 @@ export async function setMinimaxApiKey(key: string, agentDir?: string) {
provider: "minimax", provider: "minimax",
key, key,
}, },
agentDir: agentDir ?? resolveDefaultAgentDir(), agentDir: resolveAuthAgentDir(agentDir),
}); });
} }
export async function setMoonshotApiKey(key: string, agentDir?: string) { 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({ upsertAuthProfile({
profileId: "moonshot:default", profileId: "moonshot:default",
credential: { credential: {
@@ -167,12 +170,12 @@ export async function setMoonshotApiKey(key: string, agentDir?: string) {
provider: "moonshot", provider: "moonshot",
key, key,
}, },
agentDir: agentDir ?? resolveDefaultAgentDir(), agentDir: resolveAuthAgentDir(agentDir),
}); });
} }
export async function setSyntheticApiKey(key: string, agentDir?: string) { 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({ upsertAuthProfile({
profileId: "synthetic:default", profileId: "synthetic:default",
credential: { credential: {
@@ -180,7 +183,7 @@ export async function setSyntheticApiKey(key: string, agentDir?: string) {
provider: "synthetic", provider: "synthetic",
key, 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 const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto";
export async function setZaiApiKey(key: string, agentDir?: string) { 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({ upsertAuthProfile({
profileId: "zai:default", profileId: "zai:default",
credential: { credential: {
@@ -196,7 +199,7 @@ export async function setZaiApiKey(key: string, agentDir?: string) {
provider: "zai", provider: "zai",
key, key,
}, },
agentDir: agentDir ?? resolveDefaultAgentDir(), agentDir: resolveAuthAgentDir(agentDir),
}); });
} }
@@ -208,7 +211,7 @@ export async function setOpenrouterApiKey(key: string, agentDir?: string) {
provider: "openrouter", provider: "openrouter",
key, key,
}, },
agentDir: agentDir ?? resolveDefaultAgentDir(), agentDir: resolveAuthAgentDir(agentDir),
}); });
} }
@@ -714,7 +717,7 @@ export async function setOpencodeZenApiKey(key: string, agentDir?: string) {
provider: "opencode", provider: "opencode",
key, key,
}, },
agentDir: agentDir ?? resolveDefaultAgentDir(), agentDir: resolveAuthAgentDir(agentDir),
}); });
} }