From d6ce094a6083cd7ed92ff6bc2c2fca59b95f6f54 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sat, 20 Jun 2026 10:54:42 -0300 Subject: [PATCH] fix(api): migrate deprecated Codex [features].codex_hooks to [features].hooks (#4342) Codex renamed the `codex_hooks` feature flag to `hooks`; recent Codex CLI versions ignore the old key and warn. When OmniRoute rewrites an existing config.toml (configure/reset Codex provider) it now renames [features].codex_hooks -> [features].hooks, preserving the value and never clobbering an already-present `hooks`, then drops the deprecated key. The migration is a no-op when the flag is absent and runs on both the POST and DELETE config paths. Reported-by: Bian-Sh (https://github.com/decolua/9router/issues/1327) Co-authored-by: Bian-Sh <24520547+Bian-Sh@users.noreply.github.com> --- CHANGELOG.md | 1 + src/app/api/cli-tools/codex-settings/route.ts | 7 ++++ src/shared/utils/codexConfig.ts | 30 +++++++++++++++++ .../unit/codex-config-hooks-migration.test.ts | 32 +++++++++++++++++++ 4 files changed, 70 insertions(+) create mode 100644 src/shared/utils/codexConfig.ts create mode 100644 tests/unit/codex-config-hooks-migration.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c250727c0..1fb8143572 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ _In development — bullets added per PR; finalized at release._ ### 🔒 Security - **fix(mitm): exact host membership in the MITM hosts test (CodeQL false positive)** — `tests/unit/mitm-tool-hosts.test.ts` checked host membership with `Array.includes(host)`, which CodeQL's `js/incomplete-url-substring-sanitization` heuristic misreads as a `String.includes()` URL-substring sanitization test (HIGH false positive). Switched to `.some((h) => h === host)` — identical semantics, no flagged pattern. ([#4386](https://github.com/diegosouzapw/OmniRoute/pull/4386)) +- **fix(api): migrate the deprecated Codex `[features].codex_hooks` flag to `[features].hooks`** — Codex renamed the `codex_hooks` feature flag to `hooks`; recent Codex CLI versions ignore the old key and print a deprecation notice. When OmniRoute rewrites an existing `~/.codex/config.toml` (configuring/resetting the Codex provider) it now carries the user's intent forward by renaming `[features].codex_hooks` → `[features].hooks` (preserving its value, never clobbering an already-present `hooks`) and dropping the deprecated key. No-op when the flag is absent. (thanks @Bian-Sh) --- diff --git a/src/app/api/cli-tools/codex-settings/route.ts b/src/app/api/cli-tools/codex-settings/route.ts index fb4af41e67..7e890fd8ed 100644 --- a/src/app/api/cli-tools/codex-settings/route.ts +++ b/src/app/api/cli-tools/codex-settings/route.ts @@ -15,6 +15,7 @@ import { cliModelConfigSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { getApiKeyById } from "@/lib/localDb"; import { normalizeCodexBaseUrl } from "@/shared/utils/codexBaseUrl"; +import { migrateCodexFeatureFlags } from "@/shared/utils/codexConfig"; const getCodexConfigPath = () => getCliConfigPaths("codex").config; const getCodexAuthPath = () => getCliConfigPaths("codex").auth; @@ -252,6 +253,9 @@ export async function POST(request: Request) { /* No existing config */ } + // Carry the user's intent forward off the deprecated Codex feature flag (#1327). + migrateCodexFeatureFlags(parsed); + // Update only OmniRoute related fields (api_key goes to auth.json, not config.toml) parsed._root.model = model; @@ -351,6 +355,9 @@ export async function DELETE(request: Request) { throw error; } + // Carry the user's intent forward off the deprecated Codex feature flag (#1327). + migrateCodexFeatureFlags(parsed); + // Remove OmniRoute related root fields delete parsed._root.openai_base_url; diff --git a/src/shared/utils/codexConfig.ts b/src/shared/utils/codexConfig.ts new file mode 100644 index 0000000000..e86d5a5496 --- /dev/null +++ b/src/shared/utils/codexConfig.ts @@ -0,0 +1,30 @@ +/** + * Helpers for generating/maintaining the Codex CLI `config.toml`. + */ + +interface ParsedCodexToml { + _root: Record; + _sections: Record>; +} + +/** + * Migrate the deprecated Codex `[features].codex_hooks` flag to `[features].hooks`. + * + * Codex renamed the feature flag; recent Codex CLI versions ignore the old key and + * print a deprecation notice. When OmniRoute rewrites an existing `config.toml` it + * should carry the user's intent forward by renaming the key (preserving its value) + * and dropping the deprecated one. A no-op when `[features]` or `codex_hooks` is + * absent, and it never clobbers an already-present `hooks` value. (#1327) + * + * Operates in place on the route's parsed-TOML shape (`{ _root, _sections }`). + */ +export function migrateCodexFeatureFlags(parsed: ParsedCodexToml): ParsedCodexToml { + const features = parsed?._sections?.features; + if (!features || typeof features !== "object") return parsed; + if (!Object.prototype.hasOwnProperty.call(features, "codex_hooks")) return parsed; + if (!Object.prototype.hasOwnProperty.call(features, "hooks")) { + features.hooks = features.codex_hooks; + } + delete features.codex_hooks; + return parsed; +} diff --git a/tests/unit/codex-config-hooks-migration.test.ts b/tests/unit/codex-config-hooks-migration.test.ts new file mode 100644 index 0000000000..e4f70cf15f --- /dev/null +++ b/tests/unit/codex-config-hooks-migration.test.ts @@ -0,0 +1,32 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +// Regression for port-from-9router#1327: Codex deprecated the `[features].codex_hooks` +// flag in favor of `[features].hooks`. The codex-settings generator parses an existing +// config.toml and writes it back but never migrated the deprecated key, so users with an +// old config kept a key recent Codex CLI versions ignore. +const { migrateCodexFeatureFlags } = await import("../../src/shared/utils/codexConfig.ts"); + +test("#1327: renames deprecated [features].codex_hooks to [features].hooks", () => { + const parsed = { _root: {}, _sections: { features: { codex_hooks: true } } }; + migrateCodexFeatureFlags(parsed); + assert.deepEqual(parsed._sections.features, { hooks: true }); +}); + +test("#1327: keeps an existing hooks value and removes the deprecated key", () => { + const parsed = { _root: {}, _sections: { features: { codex_hooks: true, hooks: false } } }; + migrateCodexFeatureFlags(parsed); + assert.deepEqual(parsed._sections.features, { hooks: false }); +}); + +test("#1327: leaves a config that already uses hooks untouched", () => { + const parsed = { _root: {}, _sections: { features: { hooks: true } } }; + migrateCodexFeatureFlags(parsed); + assert.deepEqual(parsed._sections.features, { hooks: true }); +}); + +test("#1327: no-op when there is no [features] section", () => { + const parsed = { _root: { model: "x" }, _sections: {} }; + migrateCodexFeatureFlags(parsed); + assert.deepEqual(parsed._sections, {}); +});