Auth: add Chutes OAuth

This commit is contained in:
Friederike Seiler
2026-01-11 15:06:54 +01:00
committed by Peter Steinberger
parent 9b44c80b30
commit 4efb5cc18e
14 changed files with 1021 additions and 2 deletions

View File

@@ -19,9 +19,13 @@ describe("applyAuthChoice", () => {
const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR;
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
const previousOpenrouterKey = process.env.OPENROUTER_API_KEY;
const previousSshTty = process.env.SSH_TTY;
const previousChutesClientId = process.env.CHUTES_CLIENT_ID;
let tempStateDir: string | null = null;
afterEach(async () => {
vi.unstubAllGlobals();
if (tempStateDir) {
await fs.rm(tempStateDir, { recursive: true, force: true });
tempStateDir = null;
@@ -41,6 +45,21 @@ describe("applyAuthChoice", () => {
} else {
process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
}
if (previousOpenrouterKey === undefined) {
delete process.env.OPENROUTER_API_KEY;
} else {
process.env.OPENROUTER_API_KEY = previousOpenrouterKey;
}
if (previousSshTty === undefined) {
delete process.env.SSH_TTY;
} else {
process.env.SSH_TTY = previousSshTty;
}
if (previousChutesClientId === undefined) {
delete process.env.CHUTES_CLIENT_ID;
} else {
process.env.CHUTES_CLIENT_ID = previousChutesClientId;
}
});
it("prompts and writes MiniMax API key when selecting minimax-api", async () => {
@@ -260,4 +279,168 @@ describe("applyAuthChoice", () => {
expect(result.config.models?.providers?.["opencode-zen"]).toBeUndefined();
expect(result.agentModelOverride).toBe("opencode/claude-opus-4-5");
});
it("uses existing OPENROUTER_API_KEY when selecting openrouter-api-key", async () => {
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-"));
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent");
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
process.env.OPENROUTER_API_KEY = "sk-openrouter-test";
const text = vi.fn();
const select: WizardPrompter["select"] = vi.fn(
async (params) => params.options[0]?.value as never,
);
const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []);
const confirm = vi.fn(async () => true);
const prompter: WizardPrompter = {
intro: vi.fn(noopAsync),
outro: vi.fn(noopAsync),
note: vi.fn(noopAsync),
select,
multiselect,
text,
confirm,
progress: vi.fn(() => ({ update: noop, stop: noop })),
};
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn((code: number) => {
throw new Error(`exit:${code}`);
}),
};
const result = await applyAuthChoice({
authChoice: "openrouter-api-key",
config: {},
prompter,
runtime,
setDefaultModel: true,
});
expect(confirm).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining("OPENROUTER_API_KEY"),
}),
);
expect(text).not.toHaveBeenCalled();
expect(result.config.auth?.profiles?.["openrouter:default"]).toMatchObject({
provider: "openrouter",
mode: "api_key",
});
expect(result.config.agents?.defaults?.model?.primary).toBe(
"openrouter/auto",
);
const authProfilePath = path.join(
tempStateDir,
"agents",
"main",
"agent",
"auth-profiles.json",
);
const raw = await fs.readFile(authProfilePath, "utf8");
const parsed = JSON.parse(raw) as {
profiles?: Record<string, { key?: string }>;
};
expect(parsed.profiles?.["openrouter:default"]?.key).toBe(
"sk-openrouter-test",
);
delete process.env.OPENROUTER_API_KEY;
});
it("writes Chutes OAuth credentials when selecting chutes (remote/manual)", async () => {
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-"));
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent");
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
process.env.SSH_TTY = "1";
process.env.CHUTES_CLIENT_ID = "cid_test";
const fetchSpy = vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
if (url === "https://api.chutes.ai/idp/token") {
return new Response(
JSON.stringify({
access_token: "at_test",
refresh_token: "rt_test",
expires_in: 3600,
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
}
if (url === "https://api.chutes.ai/idp/userinfo") {
return new Response(JSON.stringify({ username: "remote-user" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
return new Response("not found", { status: 404 });
});
vi.stubGlobal("fetch", fetchSpy);
const text = vi.fn().mockResolvedValue("code_manual");
const select: WizardPrompter["select"] = vi.fn(
async (params) => params.options[0]?.value as never,
);
const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []);
const prompter: WizardPrompter = {
intro: vi.fn(noopAsync),
outro: vi.fn(noopAsync),
note: vi.fn(noopAsync),
select,
multiselect,
text,
confirm: vi.fn(async () => false),
progress: vi.fn(() => ({ update: noop, stop: noop })),
};
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn((code: number) => {
throw new Error(`exit:${code}`);
}),
};
const result = await applyAuthChoice({
authChoice: "chutes",
config: {},
prompter,
runtime,
setDefaultModel: false,
});
expect(text).toHaveBeenCalledWith(
expect.objectContaining({
message: "Paste the redirect URL (or authorization code)",
}),
);
expect(result.config.auth?.profiles?.["chutes:remote-user"]).toMatchObject({
provider: "chutes",
mode: "oauth",
});
const authProfilePath = path.join(
tempStateDir,
"agents",
"main",
"agent",
"auth-profiles.json",
);
const raw = await fs.readFile(authProfilePath, "utf8");
const parsed = JSON.parse(raw) as {
profiles?: Record<
string,
{ provider?: string; access?: string; refresh?: string; email?: string }
>;
};
expect(parsed.profiles?.["chutes:remote-user"]).toMatchObject({
provider: "chutes",
access: "at_test",
refresh: "rt_test",
email: "remote-user",
});
});
});