mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
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.
This commit is contained in:
committed by
GitHub
parent
2f7e6ec196
commit
7423e9cb66
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"id": "openai",
|
||||
"legacyPluginIds": ["openai-codex"],
|
||||
"activation": {
|
||||
"onStartup": false
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<Record<string, string>> = {
|
||||
"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");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
@@ -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<Record<string, string>> = {
|
||||
[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}\`.`;
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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<string, unknown> | 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<string> {
|
||||
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) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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<string>();
|
||||
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, unknown>): 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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<Record<string, readonly string[]>> = {
|
||||
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)) {
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
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"
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user