fix: add OpenAI Codex OAuth to configure

This commit is contained in:
Peter Steinberger
2026-01-06 09:13:04 +01:00
parent 30b6c417c7
commit cf1a1d107e
5 changed files with 179 additions and 47 deletions

View File

@@ -16,6 +16,7 @@
- Auto-reply: treat steer during compaction as a follow-up, queued until compaction completes.
- Auth: lock auth profile refreshes to avoid multi-instance OAuth logouts; keep credentials on refresh failure.
- Onboarding: prompt immediately for OpenAI Codex redirect URL on remote/headless logins.
- Configure: add OpenAI Codex (ChatGPT OAuth) auth choice (align with onboarding).
- Doctor: suggest adding the workspace memory system when missing (opt-out via `--no-workspace-suggestions`).
- Build: fix duplicate protocol export, align Codex OAuth options, and add proper-lockfile typings.
- Typing indicators: stop typing once the reply dispatcher drains to prevent stuck typing across Discord/Telegram/WhatsApp.

View File

@@ -10,7 +10,12 @@ import {
spinner,
text,
} from "@clack/prompts";
import { loginAnthropic, type OAuthCredentials } from "@mariozechner/pi-ai";
import {
loginAnthropic,
loginOpenAICodex,
type OAuthCredentials,
type OAuthProvider,
} from "@mariozechner/pi-ai";
import type { ClawdbotConfig } from "../config/config.js";
import {
CONFIG_PATH_CLAWDBOT,
@@ -54,6 +59,10 @@ import {
import { setupProviders } from "./onboard-providers.js";
import { promptRemoteGatewayConfig } from "./onboard-remote.js";
import { setupSkills } from "./onboard-skills.js";
import {
applyOpenAICodexModelDefault,
OPENAI_CODEX_DEFAULT_MODEL,
} from "./openai-codex-model-default.js";
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
type WizardSection =
@@ -234,6 +243,7 @@ async function promptAuthConfig(
message: "Model/auth choice",
options: [
{ value: "oauth", label: "Anthropic OAuth (Claude Pro/Max)" },
{ value: "openai-codex", label: "OpenAI Codex (ChatGPT OAuth)" },
{
value: "antigravity",
label: "Google Antigravity (Claude Opus 4.5, Gemini 3, etc.)",
@@ -244,7 +254,7 @@ async function promptAuthConfig(
],
}),
runtime,
) as "oauth" | "antigravity" | "apiKey" | "minimax" | "skip";
) as "oauth" | "openai-codex" | "antigravity" | "apiKey" | "minimax" | "skip";
let next = cfg;
@@ -286,6 +296,79 @@ async function promptAuthConfig(
spin.stop("OAuth failed");
runtime.error(String(err));
}
} else if (authChoice === "openai-codex") {
const isRemote = isRemoteEnvironment();
note(
isRemote
? [
"You are running in a remote/VPS environment.",
"A URL will be shown for you to open in your LOCAL browser.",
"After signing in, paste the redirect URL back here.",
].join("\n")
: [
"Browser will open for OpenAI authentication.",
"If the callback doesn't auto-complete, paste the redirect URL.",
"OpenAI OAuth uses localhost:1455 for the callback.",
].join("\n"),
"OpenAI Codex OAuth",
);
const spin = spinner();
spin.start("Starting OAuth flow…");
let manualCodePromise: Promise<string> | undefined;
try {
const creds = await loginOpenAICodex({
onAuth: async ({ url }) => {
if (isRemote) {
spin.message("OAuth URL ready (see below)…");
runtime.log(`\nOpen this URL in your LOCAL browser:\n\n${url}\n`);
manualCodePromise = text({
message: "Paste the redirect URL (or authorization code)",
validate: (value) => (value?.trim() ? undefined : "Required"),
}).then((value) => String(guardCancel(value, runtime)));
} else {
spin.message("Complete sign-in in browser…");
await openUrl(url);
runtime.log(`Open: ${url}`);
}
},
onPrompt: async (prompt) => {
if (manualCodePromise) return manualCodePromise;
const code = guardCancel(
await text({
message: prompt.message,
placeholder: prompt.placeholder,
validate: (value) => (value?.trim() ? undefined : "Required"),
}),
runtime,
);
return String(code);
},
onProgress: (msg) => spin.message(msg),
});
spin.stop("OpenAI OAuth complete");
if (creds) {
await writeOAuthCredentials(
"openai-codex" as unknown as OAuthProvider,
creds,
);
next = applyAuthProfileConfig(next, {
profileId: "openai-codex:default",
provider: "openai-codex",
mode: "oauth",
});
const applied = applyOpenAICodexModelDefault(next);
next = applied.next;
if (applied.changed) {
note(
`Default model set to ${OPENAI_CODEX_DEFAULT_MODEL}`,
"Model configured",
);
}
}
} catch (err) {
spin.stop("OpenAI OAuth failed");
runtime.error(String(err));
}
} else if (authChoice === "antigravity") {
const isRemote = isRemoteEnvironment();
note(

View File

@@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import type { ClawdbotConfig } from "../config/config.js";
import {
applyOpenAICodexModelDefault,
OPENAI_CODEX_DEFAULT_MODEL,
} from "./openai-codex-model-default.js";
describe("applyOpenAICodexModelDefault", () => {
it("sets openai-codex default when model is unset", () => {
const cfg: ClawdbotConfig = { agent: {} };
const applied = applyOpenAICodexModelDefault(cfg);
expect(applied.changed).toBe(true);
expect(applied.next.agent?.model).toEqual({
primary: OPENAI_CODEX_DEFAULT_MODEL,
});
});
it("sets openai-codex default when model is openai/*", () => {
const cfg: ClawdbotConfig = { agent: { model: "openai/gpt-5.2" } };
const applied = applyOpenAICodexModelDefault(cfg);
expect(applied.changed).toBe(true);
expect(applied.next.agent?.model).toEqual({
primary: OPENAI_CODEX_DEFAULT_MODEL,
});
});
it("does not override openai-codex/*", () => {
const cfg: ClawdbotConfig = { agent: { model: "openai-codex/gpt-5.2" } };
const applied = applyOpenAICodexModelDefault(cfg);
expect(applied.changed).toBe(false);
expect(applied.next).toEqual(cfg);
});
it("does not override non-openai models", () => {
const cfg: ClawdbotConfig = {
agent: { model: "anthropic/claude-opus-4-5" },
};
const applied = applyOpenAICodexModelDefault(cfg);
expect(applied.changed).toBe(false);
expect(applied.next).toEqual(cfg);
});
});

View File

@@ -0,0 +1,46 @@
import type { ClawdbotConfig } from "../config/config.js";
import type { AgentModelListConfig } from "../config/types.js";
export const OPENAI_CODEX_DEFAULT_MODEL = "openai-codex/gpt-5.2";
function shouldSetOpenAICodexModel(model?: string): boolean {
const trimmed = model?.trim();
if (!trimmed) return true;
const normalized = trimmed.toLowerCase();
if (normalized.startsWith("openai-codex/")) return false;
if (normalized.startsWith("openai/")) return true;
return normalized === "gpt" || normalized === "gpt-mini";
}
function resolvePrimaryModel(
model?: AgentModelListConfig | string,
): string | undefined {
if (typeof model === "string") return model;
if (model && typeof model === "object" && typeof model.primary === "string") {
return model.primary;
}
return undefined;
}
export function applyOpenAICodexModelDefault(cfg: ClawdbotConfig): {
next: ClawdbotConfig;
changed: boolean;
} {
const current = resolvePrimaryModel(cfg.agent?.model);
if (!shouldSetOpenAICodexModel(current)) {
return { next: cfg, changed: false };
}
return {
next: {
...cfg,
agent: {
...cfg.agent,
model:
cfg.agent?.model && typeof cfg.agent.model === "object"
? { ...cfg.agent.model, primary: OPENAI_CODEX_DEFAULT_MODEL }
: { primary: OPENAI_CODEX_DEFAULT_MODEL },
},
},
changed: true,
};
}

View File

@@ -52,6 +52,10 @@ import type {
OnboardOptions,
ResetScope,
} from "../commands/onboard-types.js";
import {
applyOpenAICodexModelDefault,
OPENAI_CODEX_DEFAULT_MODEL,
} from "../commands/openai-codex-model-default.js";
import { ensureSystemdUserLingerInteractive } from "../commands/systemd-linger.js";
import type { ClawdbotConfig } from "../config/config.js";
import {
@@ -60,7 +64,6 @@ import {
resolveGatewayPort,
writeConfigFile,
} from "../config/config.js";
import type { AgentModelListConfig } from "../config/types.js";
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
import { resolveGatewayService } from "../daemon/service.js";
@@ -70,50 +73,6 @@ import { defaultRuntime } from "../runtime.js";
import { resolveUserPath, sleep } from "../utils.js";
import type { WizardPrompter } from "./prompts.js";
const OPENAI_CODEX_DEFAULT_MODEL = "openai-codex/gpt-5.2";
function shouldSetOpenAICodexModel(model?: string): boolean {
const trimmed = model?.trim();
if (!trimmed) return true;
const normalized = trimmed.toLowerCase();
if (normalized.startsWith("openai-codex/")) return false;
if (normalized.startsWith("openai/")) return true;
return normalized === "gpt" || normalized === "gpt-mini";
}
function resolvePrimaryModel(
model?: AgentModelListConfig | string,
): string | undefined {
if (typeof model === "string") return model;
if (model && typeof model === "object" && typeof model.primary === "string") {
return model.primary;
}
return undefined;
}
function applyOpenAICodexModelDefault(cfg: ClawdbotConfig): {
next: ClawdbotConfig;
changed: boolean;
} {
const current = resolvePrimaryModel(cfg.agent?.model);
if (!shouldSetOpenAICodexModel(current)) {
return { next: cfg, changed: false };
}
return {
next: {
...cfg,
agent: {
...cfg.agent,
model:
cfg.agent?.model && typeof cfg.agent.model === "object"
? { ...cfg.agent.model, primary: OPENAI_CODEX_DEFAULT_MODEL }
: { primary: OPENAI_CODEX_DEFAULT_MODEL },
},
},
changed: true,
};
}
async function warnIfModelConfigLooksOff(
config: ClawdbotConfig,
prompter: WizardPrompter,