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>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-20 10:54:42 -03:00
committed by GitHub
parent 55eb1b5a14
commit d6ce094a60
4 changed files with 70 additions and 0 deletions

View File

@@ -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)
---

View File

@@ -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;

View File

@@ -0,0 +1,30 @@
/**
* Helpers for generating/maintaining the Codex CLI `config.toml`.
*/
interface ParsedCodexToml {
_root: Record<string, unknown>;
_sections: Record<string, Record<string, unknown>>;
}
/**
* 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;
}

View File

@@ -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, {});
});