From 7423e9cb663c2c1bffff2ca4fa748ad60993d11c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 31 May 2026 14:03:17 +0100 Subject: [PATCH] refactor(openai): confine legacy codex repair to doctor Confine retired OpenAI Codex identifiers to doctor repair and migration paths while keeping runtime OpenAI surfaces canonical.\n\nProof: focused Vitest; autoreview clean; AWS Crabbox check:changed run_3789cbe12413 (cbx_2c88b700810b) passed. --- extensions/openai/openclaw.plugin.json | 1 - extensions/openai/openclaw.plugin.test.ts | 2 +- scripts/crabbox-wrapper.mjs | 1 + scripts/generate-plugin-inventory-doc.mjs | 3 - .../oauth-refresh-failure.test.ts | 3 - .../auth-profiles/oauth-refresh-failure.ts | 10 +- src/agents/session-file-repair.test.ts | 30 ------ src/agents/session-file-repair.ts | 11 +-- src/commands/doctor-auth.ts | 8 +- .../doctor-session-transcripts.test.ts | 36 +++++++- src/commands/doctor-session-transcripts.ts | 85 +++++++++++++++-- .../shared/legacy-config-migrate.test.ts | 29 ++++++ ...acy-config-migrations.runtime.providers.ts | 91 +++++++++++++++++++ .../shared/plugin-registry-migration.test.ts | 32 +++++++ .../shared/plugin-registry-migration.ts | 4 + src/plugins/provider-openai-chatgpt-oauth.ts | 4 +- test/scripts/crabbox-wrapper.test.ts | 12 ++- 17 files changed, 288 insertions(+), 74 deletions(-) diff --git a/extensions/openai/openclaw.plugin.json b/extensions/openai/openclaw.plugin.json index 91f2b89e995..514fe91be23 100644 --- a/extensions/openai/openclaw.plugin.json +++ b/extensions/openai/openclaw.plugin.json @@ -1,6 +1,5 @@ { "id": "openai", - "legacyPluginIds": ["openai-codex"], "activation": { "onStartup": false }, diff --git a/extensions/openai/openclaw.plugin.test.ts b/extensions/openai/openclaw.plugin.test.ts index 10393f5cf9e..80c04fc9647 100644 --- a/extensions/openai/openclaw.plugin.test.ts +++ b/extensions/openai/openclaw.plugin.test.ts @@ -106,7 +106,7 @@ describe("OpenAI plugin manifest", () => { }); it("routes setup through the OpenAI setup runtime", () => { - expect(manifest.legacyPluginIds).toEqual(["openai-codex"]); + expect(manifest.legacyPluginIds).toBeUndefined(); expect(manifest.setup?.providers?.map((provider) => provider.id)).toEqual(["openai"]); expect(manifest.providerAuthAliases).toBeUndefined(); }); diff --git a/scripts/crabbox-wrapper.mjs b/scripts/crabbox-wrapper.mjs index 3fff8384e31..5d1f295d49b 100755 --- a/scripts/crabbox-wrapper.mjs +++ b/scripts/crabbox-wrapper.mjs @@ -143,6 +143,7 @@ const shellInlineCommandInterpreters = new Set(["bash", "dash", "ksh", "sh", "zs const remoteChangedGateEnv = [ "OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1", "OPENCLAW_CHANGED_LANES_RAW_SYNC=1", + "CI=1", ]; const shellInlineCommandOptionsWithNextValue = new Set([ "+O", diff --git a/scripts/generate-plugin-inventory-doc.mjs b/scripts/generate-plugin-inventory-doc.mjs index 68cd93da4a4..0eca9599bee 100644 --- a/scripts/generate-plugin-inventory-doc.mjs +++ b/scripts/generate-plugin-inventory-doc.mjs @@ -167,9 +167,6 @@ function resolveDescription({ manifest, packageJson }) { const providers = Array.isArray(manifest.providers) ? manifest.providers : []; if (providers.length > 0) { - if (manifest.providerAuthAliases?.["openai-codex"] === "openai") { - return `Adds ${displayList(providers)} model provider support to OpenClaw, including ChatGPT/Codex OAuth.`; - } return `Adds ${displayList(providers)} model provider support to OpenClaw.`; } diff --git a/src/agents/auth-profiles/oauth-refresh-failure.test.ts b/src/agents/auth-profiles/oauth-refresh-failure.test.ts index 8a80898c3c1..41725cc7101 100644 --- a/src/agents/auth-profiles/oauth-refresh-failure.test.ts +++ b/src/agents/auth-profiles/oauth-refresh-failure.test.ts @@ -15,8 +15,5 @@ describe("oauth refresh failure hints", () => { expect(buildOAuthRefreshFailureLoginCommand("openai")).toBe( "openclaw models auth login --provider openai", ); - expect(buildOAuthRefreshFailureLoginCommand("OpenAI-Codex")).toBe( - "openclaw models auth login --provider openai", - ); }); }); diff --git a/src/agents/auth-profiles/oauth-refresh-failure.ts b/src/agents/auth-profiles/oauth-refresh-failure.ts index be5ec2315b4..98c06c51acf 100644 --- a/src/agents/auth-profiles/oauth-refresh-failure.ts +++ b/src/agents/auth-profiles/oauth-refresh-failure.ts @@ -11,9 +11,6 @@ export type OAuthRefreshFailureReason = const OAUTH_REFRESH_FAILURE_PROVIDER_RE = /OAuth token refresh failed for ([^:]+):/i; const SAFE_PROVIDER_ID_RE = /^[a-z0-9][a-z0-9._-]*$/; -const RETIRED_REAUTH_PROVIDER_IDS: Readonly> = { - "openai-codex": "openai", -}; function isOAuthRefreshFailureMessage(message: string): boolean { const lower = message.toLowerCase(); @@ -72,10 +69,7 @@ export function classifyOAuthRefreshFailure(message: string): { export function buildOAuthRefreshFailureLoginCommand(provider: string | null | undefined): string { const sanitizedProvider = sanitizeOAuthRefreshFailureProvider(provider); - const reauthProvider = sanitizedProvider - ? (RETIRED_REAUTH_PROVIDER_IDS[sanitizedProvider] ?? sanitizedProvider) - : null; - return reauthProvider - ? formatCliCommand(`openclaw models auth login --provider ${reauthProvider}`) + return sanitizedProvider + ? formatCliCommand(`openclaw models auth login --provider ${sanitizedProvider}`) : formatCliCommand("openclaw models auth login"); } diff --git a/src/agents/session-file-repair.test.ts b/src/agents/session-file-repair.test.ts index fabc2fcafec..dd2f0922dba 100644 --- a/src/agents/session-file-repair.test.ts +++ b/src/agents/session-file-repair.test.ts @@ -583,36 +583,6 @@ describe("repairSessionFileIfNeeded", () => { expect(JSON.parse(lines[4])).toEqual(deliveryMirror); }); - it("repairs missing tool results in legacy OpenAI Codex transcripts", async () => { - const { file } = await createTempSessionPath(); - const { header, message } = buildSessionHeaderAndMessage(); - const toolCallAssistant = { - type: "message", - id: "msg-asst-legacy-process", - parentId: "msg-1", - timestamp: new Date().toISOString(), - message: { - role: "assistant", - provider: "openai-codex", - model: "gpt-5.5", - api: "openai-codex-responses", - content: [{ type: "toolCall", id: "call_process|fc_1", name: "process", arguments: {} }], - stopReason: "toolUse", - }, - }; - const original = `${JSON.stringify(header)}\n${JSON.stringify(message)}\n${JSON.stringify(toolCallAssistant)}\n`; - await fs.writeFile(file, original, "utf-8"); - - const result = await repairSessionFileIfNeeded({ sessionFile: file }); - - expect(result.repaired).toBe(true); - expect(result.insertedToolResults).toBe(1); - const lines = (await fs.readFile(file, "utf-8")).trimEnd().split("\n"); - const inserted = JSON.parse(lines[3]); - expect(inserted.message.role).toBe("toolResult"); - expect(inserted.message.toolCallId).toBe("call_process|fc_1"); - }); - it("does not duplicate code-mode tool results that are already persisted", async () => { const { file } = await createTempSessionPath(); const { header, message } = buildSessionHeaderAndMessage(); diff --git a/src/agents/session-file-repair.ts b/src/agents/session-file-repair.ts index 5918ee45208..d16d6ef16f0 100644 --- a/src/agents/session-file-repair.ts +++ b/src/agents/session-file-repair.ts @@ -206,17 +206,10 @@ function isCodeModeToolCallRepairCandidate(entry: unknown): entry is SessionMess provider?: unknown; stopReason?: unknown; }; - // Persisted transcripts from the retired OpenAI Codex route still need this - // repair so replay sees a complete tool-call/tool-result pair. - const legacyOpenAIProvider = "openai-codex"; - const legacyOpenAIResponsesApi = "openai-codex-responses"; - const openAIProvider = message.provider === "openai" || message.provider === legacyOpenAIProvider; - const openAIResponsesApi = - message.api === "openai-chatgpt-responses" || message.api === legacyOpenAIResponsesApi; return ( message.role === "assistant" && - openAIResponsesApi && - openAIProvider && + message.api === "openai-chatgpt-responses" && + message.provider === "openai" && message.stopReason !== "error" && message.stopReason !== "aborted" ); diff --git a/src/commands/doctor-auth.ts b/src/commands/doctor-auth.ts index 01f7b0ccdd2..6142f406a44 100644 --- a/src/commands/doctor-auth.ts +++ b/src/commands/doctor-auth.ts @@ -41,6 +41,9 @@ const LEGACY_CODEX_PROVIDER_ID = "openai-codex"; const CODEX_OAUTH_WARNING_TITLE = "Codex OAuth"; const OPENAI_BASE_URL = "https://api.openai.com/v1"; const LEGACY_CODEX_APIS = new Set(["openai-responses", "openai-completions"]); +const DOCTOR_REAUTH_PROVIDER_ALIASES: Readonly> = { + [LEGACY_CODEX_PROVIDER_ID]: OPENAI_PROVIDER_ID, +}; function hasConfiguredCodexOAuthProfile(cfg: OpenClawConfig): boolean { return Object.values(cfg.auth?.profiles ?? {}).some( @@ -221,7 +224,10 @@ export function formatOAuthRefreshFailureDoctorLine(params: { if (!classified) { return null; } - const provider = classified.provider ?? params.provider; + const rawProvider = classified.provider ?? params.provider; + const provider = rawProvider + ? (DOCTOR_REAUTH_PROVIDER_ALIASES[rawProvider] ?? rawProvider) + : null; const command = buildOAuthRefreshFailureLoginCommand(provider); if (classified.reason) { return `- ${params.profileId}: re-auth required [${formatOAuthRefreshFailureReason(classified.reason)}] — Run \`${command}\`.`; diff --git a/src/commands/doctor-session-transcripts.test.ts b/src/commands/doctor-session-transcripts.test.ts index fd0880aef3e..833fd727173 100644 --- a/src/commands/doctor-session-transcripts.test.ts +++ b/src/commands/doctor-session-transcripts.test.ts @@ -143,11 +143,45 @@ describe("doctor session transcript repair", () => { expect(note).toHaveBeenCalledTimes(1); const [message, title] = requireFirstMockCall(note, "doctor note") as [string, string]; expect(title).toBe("Session transcripts"); - expect(message).toContain("duplicated prompt-rewrite branches"); + expect(message).toContain("legacy state"); expect(message).toContain('Run "openclaw doctor --fix"'); expect(countNonEmptyLines(await fs.readFile(filePath, "utf-8"))).toBe(3); }); + it("rewrites legacy OpenAI Codex transcript metadata only during doctor repair", async () => { + const filePath = await writeTranscript([ + { type: "session", version: 3, id: "session-1", timestamp: "2026-04-25T00:00:00Z" }, + { + type: "message", + id: "legacy-assistant", + parentId: null, + message: { + role: "assistant", + provider: "openai-codex", + api: "openai-codex-responses", + content: [{ type: "text", text: "hello" }], + }, + }, + ]); + + const preview = await repairBrokenSessionTranscriptFile({ filePath, shouldRepair: false }); + + expect(preview.broken).toBe(true); + expect(preview.repaired).toBe(false); + expect(preview.legacyOpenAICodexEntries).toBe(1); + expect(await fs.readFile(filePath, "utf-8")).toContain("openai-codex"); + + const result = await repairBrokenSessionTranscriptFile({ filePath, shouldRepair: true }); + + expect(result.broken).toBe(true); + expect(result.repaired).toBe(true); + expect(result.legacyOpenAICodexEntries).toBe(1); + const lines = (await fs.readFile(filePath, "utf-8")).trim().split(/\r?\n/); + const assistant = JSON.parse(lines[1]); + expect(assistant.message.provider).toBe("openai"); + expect(assistant.message.api).toBe("openai-chatgpt-responses"); + }); + it("ignores ordinary branch history without internal runtime context", async () => { const filePath = await writeTranscript([ { type: "session", version: 3, id: "session-1", timestamp: "2026-04-25T00:00:00Z" }, diff --git a/src/commands/doctor-session-transcripts.ts b/src/commands/doctor-session-transcripts.ts index dc55469433d..7ca72e38354 100644 --- a/src/commands/doctor-session-transcripts.ts +++ b/src/commands/doctor-session-transcripts.ts @@ -23,10 +23,16 @@ type TranscriptRepairResult = { repaired: boolean; originalEntries: number; activeEntries: number; + legacyOpenAICodexEntries: number; backupPath?: string; reason?: string; }; +const LEGACY_OPENAI_CODEX_PROVIDER_ID = "openai-codex"; +const OPENAI_PROVIDER_ID = "openai"; +const LEGACY_OPENAI_CODEX_RESPONSES_API = "openai-codex-responses"; +const OPENAI_CHATGPT_RESPONSES_API = "openai-chatgpt-responses"; + function parseTranscriptEntries(raw: string): TranscriptEntry[] { const entries: TranscriptEntry[] = []; for (const line of raw.split(/\r?\n/)) { @@ -59,6 +65,29 @@ function getMessage(entry: TranscriptEntry): Record | null { : null; } +function normalizeLegacyOpenAICodexTranscriptMetadata(entries: TranscriptEntry[]): number { + let changed = 0; + for (const entry of entries) { + const message = getMessage(entry); + if (!message) { + continue; + } + let touched = false; + if (message.provider === LEGACY_OPENAI_CODEX_PROVIDER_ID) { + message.provider = OPENAI_PROVIDER_ID; + touched = true; + } + if (message.api === LEGACY_OPENAI_CODEX_RESPONSES_API) { + message.api = OPENAI_CHATGPT_RESPONSES_API; + touched = true; + } + if (touched) { + changed += 1; + } + } + return changed; +} + function textFromContent(content: unknown): string | null { if (typeof content === "string") { return content; @@ -166,6 +195,19 @@ async function writeActiveTranscript(params: { return backupPath; } +async function writeTranscriptEntries(params: { + filePath: string; + entries: TranscriptEntry[]; +}): Promise { + const backupPath = `${params.filePath}.pre-doctor-openai-codex-repair-${new Date() + .toISOString() + .replace(/[:.]/g, "-")}.bak`; + await fs.copyFile(params.filePath, backupPath); + const next = params.entries.map((entry) => JSON.stringify(entry)).join("\n"); + await fs.writeFile(params.filePath, `${next}\n`, "utf-8"); + return backupPath; +} + export async function repairBrokenSessionTranscriptFile(params: { filePath: string; shouldRepair: boolean; @@ -173,25 +215,41 @@ export async function repairBrokenSessionTranscriptFile(params: { try { const raw = await fs.readFile(params.filePath, "utf-8"); const entries = parseTranscriptEntries(raw); + const legacyOpenAICodexEntries = normalizeLegacyOpenAICodexTranscriptMetadata(entries); const activePath = selectActivePath(entries); if (!activePath) { + if (legacyOpenAICodexEntries > 0 && params.shouldRepair) { + const backupPath = await writeTranscriptEntries({ filePath: params.filePath, entries }); + return { + filePath: params.filePath, + broken: true, + repaired: true, + originalEntries: entries.length, + activeEntries: 0, + legacyOpenAICodexEntries, + backupPath, + reason: "no active branch", + }; + } return { filePath: params.filePath, - broken: false, + broken: legacyOpenAICodexEntries > 0, repaired: false, originalEntries: entries.length, activeEntries: 0, + legacyOpenAICodexEntries, reason: "no active branch", }; } const broken = hasBrokenPromptRewriteBranch(entries, activePath); - if (!broken) { + if (!broken && legacyOpenAICodexEntries === 0) { return { filePath: params.filePath, broken: false, repaired: false, originalEntries: entries.length, activeEntries: activePath.length, + legacyOpenAICodexEntries, }; } if (!params.shouldRepair) { @@ -201,19 +259,23 @@ export async function repairBrokenSessionTranscriptFile(params: { repaired: false, originalEntries: entries.length, activeEntries: activePath.length, + legacyOpenAICodexEntries, }; } - const backupPath = await writeActiveTranscript({ - filePath: params.filePath, - entries, - activePath, - }); + const backupPath = broken + ? await writeActiveTranscript({ + filePath: params.filePath, + entries, + activePath, + }) + : await writeTranscriptEntries({ filePath: params.filePath, entries }); return { filePath: params.filePath, broken: true, repaired: true, originalEntries: entries.length, activeEntries: activePath.length, + legacyOpenAICodexEntries, backupPath, }; } catch (err) { @@ -223,6 +285,7 @@ export async function repairBrokenSessionTranscriptFile(params: { repaired: false, originalEntries: 0, activeEntries: 0, + legacyOpenAICodexEntries: 0, reason: String(err), }; } @@ -275,11 +338,15 @@ export async function noteSessionTranscriptHealth(params?: { const repairedCount = broken.filter((result) => result.repaired).length; const lines = [ - `- Found ${broken.length} transcript file${broken.length === 1 ? "" : "s"} with duplicated prompt-rewrite branches.`, + `- Found ${broken.length} transcript file${broken.length === 1 ? "" : "s"} with legacy state.`, ...broken.slice(0, 20).map((result) => { const backup = result.backupPath ? ` backup=${shortenHomePath(result.backupPath)}` : ""; const status = result.repaired ? "repaired" : "needs repair"; - return `- ${shortenHomePath(result.filePath)} ${status} entries=${result.originalEntries}->${result.activeEntries + 1}${backup}`; + const metadata = + result.legacyOpenAICodexEntries > 0 + ? ` openai-codex=${result.legacyOpenAICodexEntries}` + : ""; + return `- ${shortenHomePath(result.filePath)} ${status} entries=${result.originalEntries}->${result.activeEntries + 1}${metadata}${backup}`; }), ]; if (broken.length > 20) { diff --git a/src/commands/doctor/shared/legacy-config-migrate.test.ts b/src/commands/doctor/shared/legacy-config-migrate.test.ts index c594c4fc916..6a656700102 100644 --- a/src/commands/doctor/shared/legacy-config-migrate.test.ts +++ b/src/commands/doctor/shared/legacy-config-migrate.test.ts @@ -1412,6 +1412,35 @@ describe("legacy migrate x_search auth", () => { }); describe("legacy bundled provider discovery migrate", () => { + it("rewrites legacy OpenAI Codex plugin policy ids", () => { + const res = migrateLegacyConfigForTest({ + plugins: { + allow: ["telegram", "openai-codex", "openai"], + deny: ["openai-codex"], + entries: { + "openai-codex": { + enabled: false, + }, + }, + slots: { + memory: "openai-codex", + }, + }, + }); + + expect(res.config?.plugins?.allow).toEqual(["telegram", "openai"]); + expect(res.config?.plugins?.deny).toEqual(["openai"]); + expect(res.config?.plugins?.entries?.openai).toEqual({ enabled: false }); + expect(res.config?.plugins?.entries?.["openai-codex"]).toBeUndefined(); + expect(res.config?.plugins?.slots?.memory).toBe("openai"); + expect(res.changes).toContain("Rewrote plugins.allow openai-codex references to openai."); + expect(res.changes).toContain("Rewrote plugins.deny openai-codex references to openai."); + expect(res.changes).toContain( + "Rewrote plugins.entries.openai-codex to plugins.entries.openai.", + ); + expect(res.changes).toContain("Rewrote plugins.slots openai-codex references to openai."); + }); + it("sets compat mode for existing restrictive plugin allowlists", () => { const res = migrateLegacyConfigForTest({ plugins: { diff --git a/src/commands/doctor/shared/legacy-config-migrations.runtime.providers.ts b/src/commands/doctor/shared/legacy-config-migrations.runtime.providers.ts index 2724cfcb50f..250926a0a1f 100644 --- a/src/commands/doctor/shared/legacy-config-migrations.runtime.providers.ts +++ b/src/commands/doctor/shared/legacy-config-migrations.runtime.providers.ts @@ -6,6 +6,9 @@ import { import { isRecord } from "./legacy-config-record-shared.js"; import { migrateLegacyXSearchConfig } from "./legacy-x-search-migrate.js"; +const LEGACY_OPENAI_CODEX_PLUGIN_ID = "openai-codex"; +const OPENAI_PLUGIN_ID = "openai"; + const BUNDLED_DISCOVERY_COMPAT_RULE: LegacyConfigRule = { path: ["plugins", "allow"], message: @@ -26,7 +29,95 @@ const X_SEARCH_RULE: LegacyConfigRule = { 'tools.web.x_search.apiKey moved to the xAI plugin; use plugins.entries.xai.config.webSearch.apiKey instead. Run "openclaw doctor --fix".', }; +function rewritePluginIdList(value: unknown): { next: unknown; changed: boolean } { + if (!Array.isArray(value)) { + return { next: value, changed: false }; + } + let changed = false; + const seen = new Set(); + const next: unknown[] = []; + for (const entry of value) { + const replacement = entry === LEGACY_OPENAI_CODEX_PLUGIN_ID ? OPENAI_PLUGIN_ID : entry; + if (replacement !== entry) { + changed = true; + } + if (typeof replacement === "string") { + if (seen.has(replacement)) { + changed = true; + continue; + } + seen.add(replacement); + } + next.push(replacement); + } + return { next, changed }; +} + +function rewritePluginSlots(value: unknown): boolean { + if (!isRecord(value)) { + return false; + } + let changed = false; + for (const [slot, pluginId] of Object.entries(value)) { + if (pluginId === LEGACY_OPENAI_CODEX_PLUGIN_ID) { + value[slot] = OPENAI_PLUGIN_ID; + changed = true; + } + } + return changed; +} + +function rewritePluginEntries(value: unknown): boolean { + if (!isRecord(value) || !(LEGACY_OPENAI_CODEX_PLUGIN_ID in value)) { + return false; + } + if (!(OPENAI_PLUGIN_ID in value)) { + value[OPENAI_PLUGIN_ID] = value[LEGACY_OPENAI_CODEX_PLUGIN_ID]; + } + delete value[LEGACY_OPENAI_CODEX_PLUGIN_ID]; + return true; +} + +function rewriteLegacyOpenAICodexPluginPolicy(raw: Record): string[] { + const plugins = isRecord(raw.plugins) ? raw.plugins : undefined; + if (!plugins) { + return []; + } + const changes: string[] = []; + for (const key of ["allow", "deny"] as const) { + const rewritten = rewritePluginIdList(plugins[key]); + if (rewritten.changed) { + plugins[key] = rewritten.next; + changes.push(`Rewrote plugins.${key} openai-codex references to openai.`); + } + } + if (rewritePluginEntries(plugins.entries)) { + changes.push("Rewrote plugins.entries.openai-codex to plugins.entries.openai."); + } + if (rewritePluginSlots(plugins.slots)) { + changes.push("Rewrote plugins.slots openai-codex references to openai."); + } + return changes; +} + export const LEGACY_CONFIG_MIGRATIONS_RUNTIME_PROVIDERS: LegacyConfigMigrationSpec[] = [ + defineLegacyConfigMigration({ + id: "plugins.openai-codex->plugins.openai", + describe: "Rewrite retired OpenAI Codex plugin policy ids", + legacyRules: [ + { + path: ["plugins"], + message: + 'plugins.openai-codex references are retired; use the openai plugin id. Run "openclaw doctor --fix".', + requireSourceLiteral: true, + match: (_value, root) => + rewriteLegacyOpenAICodexPluginPolicy(structuredClone(root)).length > 0, + }, + ], + apply: (raw, changes) => { + changes.push(...rewriteLegacyOpenAICodexPluginPolicy(raw)); + }, + }), defineLegacyConfigMigration({ id: "plugins.allow->plugins.bundledDiscovery.compat", describe: "Preserve bundled provider discovery for existing restrictive allowlists", diff --git a/src/commands/doctor/shared/plugin-registry-migration.test.ts b/src/commands/doctor/shared/plugin-registry-migration.test.ts index 138084532e0..9c8ae78ab35 100644 --- a/src/commands/doctor/shared/plugin-registry-migration.test.ts +++ b/src/commands/doctor/shared/plugin-registry-migration.test.ts @@ -247,6 +247,38 @@ describe("plugin registry install migration", () => { expect(persisted?.plugins.map((plugin) => plugin.pluginId)).toEqual(["openai"]); }); + it("keeps legacy OpenAI Codex plugin references doctor-only", async () => { + const stateDir = makeTempDir(); + const openaiDir = path.join(stateDir, "plugins", "openai"); + const unusedBundledDir = path.join(stateDir, "plugins", "unused-bundled"); + fs.mkdirSync(openaiDir, { recursive: true }); + fs.mkdirSync(unusedBundledDir, { recursive: true }); + + const result = await migratePluginRegistryForInstall({ + stateDir, + candidates: [ + createCandidate(openaiDir, "openai", "bundled"), + createCandidate(unusedBundledDir, "unused-bundled", "bundled"), + ], + readConfig: async () => ({ + plugins: { + entries: { + "openai-codex": { + enabled: true, + }, + }, + }, + }), + env: hermeticEnv(), + }); + + const current = requireMigratedIndex(result); + expect(current.plugins.map((plugin) => plugin.pluginId)).toEqual(["openai"]); + + const persisted = await readPersistedInstalledPluginIndex({ stateDir }); + expect(persisted?.plugins.map((plugin) => plugin.pluginId)).toEqual(["openai"]); + }); + it("keeps bundled memory command plugins discoverable for first-run CLI registration", async () => { const stateDir = makeTempDir(); const memoryDir = path.join(stateDir, "plugins", "memory-core"); diff --git a/src/commands/doctor/shared/plugin-registry-migration.ts b/src/commands/doctor/shared/plugin-registry-migration.ts index c9988095d25..3ce3def0543 100644 --- a/src/commands/doctor/shared/plugin-registry-migration.ts +++ b/src/commands/doctor/shared/plugin-registry-migration.ts @@ -25,6 +25,9 @@ import type { PluginManifestRecord } from "../../../plugins/manifest-registry.js export const DISABLE_PLUGIN_REGISTRY_MIGRATION_ENV = "OPENCLAW_DISABLE_PLUGIN_REGISTRY_MIGRATION"; export const FORCE_PLUGIN_REGISTRY_MIGRATION_ENV = "OPENCLAW_FORCE_PLUGIN_REGISTRY_MIGRATION"; +const DOCTOR_PLUGIN_ID_ALIASES: Readonly> = { + openai: ["openai-codex"], +}; export type PluginRegistryInstallMigrationPreflightAction = | "disabled" @@ -150,6 +153,7 @@ function createMigrationPluginIdNormalizer( ...(plugin.setup?.cliBackends ?? []), ...Object.keys(plugin.modelCatalog?.providers ?? {}), ...(plugin.legacyPluginIds ?? []), + ...(DOCTOR_PLUGIN_ID_ALIASES[plugin.id] ?? []), ]) { const normalizedAlias = normalizeRegistryReference(alias); if (normalizedAlias && !aliases.has(normalizedAlias)) { diff --git a/src/plugins/provider-openai-chatgpt-oauth.ts b/src/plugins/provider-openai-chatgpt-oauth.ts index 2a1e2a0b4a8..5d936d29cb3 100644 --- a/src/plugins/provider-openai-chatgpt-oauth.ts +++ b/src/plugins/provider-openai-chatgpt-oauth.ts @@ -7,7 +7,6 @@ import { createVpsAwareOAuthHandlers } from "./provider-oauth-flow.js"; import type { ProviderAuthContext } from "./types.js"; const OPENAI_CODEX_PROVIDER_ID = "openai"; -const OPENAI_CODEX_LEGACY_PROVIDER_ID = "openai-codex"; const OPENAI_CODEX_OAUTH_METHOD_ID = "oauth"; type OpenAICodexOAuthBridgeContext = ProviderAuthContext & { @@ -45,8 +44,7 @@ function isOAuthCredential(value: unknown): value is OAuthCredentials { const record = value as Record; return ( record.type === "oauth" && - (record.provider === OPENAI_CODEX_PROVIDER_ID || - record.provider === OPENAI_CODEX_LEGACY_PROVIDER_ID) && + record.provider === OPENAI_CODEX_PROVIDER_ID && typeof record.access === "string" && typeof record.refresh === "string" && typeof record.expires === "number" diff --git a/test/scripts/crabbox-wrapper.test.ts b/test/scripts/crabbox-wrapper.test.ts index 10d8400acb8..f663f6c3981 100644 --- a/test/scripts/crabbox-wrapper.test.ts +++ b/test/scripts/crabbox-wrapper.test.ts @@ -312,7 +312,7 @@ function expectGroupedShellCommand(remoteCommand: string, command: string): void } const remoteChangedGateEnvPrefix = - "OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1"; + "OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 CI=1"; const remoteChangedGateExport = `export ${remoteChangedGateEnvPrefix};`; afterAll(() => { @@ -1758,7 +1758,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => { expect(remoteCommand).toContain("git diff --cached --quiet"); expect(remoteCommand).toContain("commit -q --no-gpg-sign -m remote-changed-gate-tree"); expect(remoteCommand).toMatch( - /&& env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed$/u, + /&& env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 CI=1 corepack pnpm check:changed$/u, ); }); @@ -1935,7 +1935,9 @@ describe.concurrent("scripts/crabbox-wrapper", () => { expect(remoteCommand).toContain( "git fetch -q --depth=1 origin abc123:refs/remotes/origin/main", ); - expect(remoteCommand).toContain(`&& ${remoteChangedGateExport} ${shellScript}`); + expect(remoteCommand).toContain( + `&& export OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1; ${shellScript}`, + ); }); it("preserves sparse changed-gate Git bootstrap for shell option values before -c", () => { @@ -2079,7 +2081,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => { expect(output.args).toContain("--shell"); expect(remoteCommand).toContain("git init -q"); expect(remoteCommand).toMatch( - /&& env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 timeout 1200s node scripts\/check-changed\.mjs --base origin\/main --head HEAD$/u, + /&& env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 CI=1 timeout 1200s node scripts\/check-changed\.mjs --base origin\/main --head HEAD$/u, ); }); @@ -2102,7 +2104,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => { expect(output.args).toContain("--shell"); expect(remoteCommand).toContain("git init -q"); expect(remoteCommand).toMatch( - /&& env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 timeout 1200s bash -lc 'pnpm check:changed'$/u, + /&& env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 CI=1 timeout 1200s bash -lc 'pnpm check:changed'$/u, ); });