diff --git a/README.md b/README.md index 7eaf25ce5..9816a18f2 100644 --- a/README.md +++ b/README.md @@ -293,6 +293,7 @@ PatewayAI 专注于官方高质量模型 API 中继,完整支持 Claude、Code - Skills 与插件:`/skills/` 路由以独立 Tab 管理 Skills 安装和 Codex 插件安装;Skills 可从内置/自定义 GitHub 仓库或 skills.sh 搜索后单独安装,也支持 ZIP、目录导入和安全卸载;Codex 原生 Marketplace 继续负责完整插件安装,系统 Skill 只读 - 项目启动:桌面端收藏本机目录;Windows / macOS 直接在 ChatGPT Codex App 中打开对应工作区,“会话”通过本机 CLI 继续项目;Web / Docker 不访问设备目录 - 设置页:统一管理端口、监听地址、代理、请求超时、SSE 保活、主题、自动更新、后台行为 +- 基础设置中的“移除 requires_openai_auth”默认开启,保持托管网关的图片/actor 扩展认证行为。关闭后仅在切换到网关、重复应用或启动同步时保留 `cm` provider 已有的值,不会恢复此前删除的字段;保留 `true` 可能禁用 Codex 图片扩展。此设置不影响直连聚合 API,也不改写其他 provider。 ### Service 版 - `codexmanager-service`:提供本地 OpenAI 兼容网关 diff --git a/apps/src/app/settings/components/general-basics-card.tsx b/apps/src/app/settings/components/general-basics-card.tsx index 734a6e4d0..fdfd79993 100644 --- a/apps/src/app/settings/components/general-basics-card.tsx +++ b/apps/src/app/settings/components/general-basics-card.tsx @@ -16,6 +16,7 @@ import type { AppSettings } from "@/types"; type GeneralBasicsSnapshot = Pick< AppSettings, | "updateAutoCheck" + | "removeRequiresOpenaiAuth" | "autoStartEnabled" | "autoStartSupported" | "showMainWindowOnStartup" @@ -157,6 +158,20 @@ export function GeneralBasicsCard({ } /> +
+
+ +

+ {t("默认开启,在切换到网关或同步配置时清理旧认证标记;关闭仅保留已有值,不恢复已删除的配置。保留 true 可能禁用图片扩展。")} +

+
+ updateSettings.mutate({ removeRequiresOpenaiAuth: value })} + /> +
diff --git a/apps/src/lib/api/normalize.ts b/apps/src/lib/api/normalize.ts index 137b28f3d..33b4d4ff3 100644 --- a/apps/src/lib/api/normalize.ts +++ b/apps/src/lib/api/normalize.ts @@ -1900,6 +1900,7 @@ export function normalizeAppSettings(payload: unknown): AppSettings { ); return { updateAutoCheck: asBoolean(source.updateAutoCheck, true), + removeRequiresOpenaiAuth: asBoolean(source.removeRequiresOpenaiAuth, true), autoStartEnabled: asBoolean(source.autoStartEnabled, false), autoStartSupported: asBoolean(source.autoStartSupported, false), showMainWindowOnStartup: asBoolean(source.showMainWindowOnStartup, true), diff --git a/apps/src/lib/i18n/messages/en.ts b/apps/src/lib/i18n/messages/en.ts index 1d2fb24df..0a17c4ce2 100644 --- a/apps/src/lib/i18n/messages/en.ts +++ b/apps/src/lib/i18n/messages/en.ts @@ -184,6 +184,9 @@ export const EN_MESSAGES: MessageCatalog = { 基础设置: "Basic settings", 控制应用启动和窗口行为: "Control startup and window behavior.", 自动检查更新: "Check updates automatically", + "移除 requires_openai_auth": "Remove requires_openai_auth", + "默认开启,在切换到网关或同步配置时清理旧认证标记;关闭仅保留已有值,不恢复已删除的配置。保留 true 可能禁用图片扩展。": + "Enabled by default: clear the old auth flag when switching to the gateway or syncing the profile. Disabling only preserves existing values; it does not restore removed settings. Keeping true may disable image extensions.", "启动完成后在后台检查更新,并每 7 小时检查一次": "Check for updates in the background after startup, then every 7 hours.", "检测到新版本,是否现在更新?": diff --git a/apps/src/lib/i18n/messages/ko.ts b/apps/src/lib/i18n/messages/ko.ts index a2decd45f..cee4d0240 100644 --- a/apps/src/lib/i18n/messages/ko.ts +++ b/apps/src/lib/i18n/messages/ko.ts @@ -198,6 +198,9 @@ export const KO_MESSAGES: MessageCatalog = { "CodexManager는 Codex CLI 계정, 로컬 게이트웨이, 플랫폼 키, 요청 로그, 사용량 통계를 통합 관리하여 Codex 연결과 운영을 더 집중적이고 제어 가능하게 합니다.", 基础设置: "기본 설정", 自动检查更新: "자동 업데이트 확인", + "移除 requires_openai_auth": "requires_openai_auth 제거", + "默认开启,在切换到网关或同步配置时清理旧认证标记;关闭仅保留已有值,不恢复已删除的配置。保留 true 可能禁用图片扩展。": + "기본적으로 켜져 있으며 게이트웨이 전환 또는 프로필 동기화 시 이전 인증 플래그를 제거합니다. 끄면 기존 값만 유지하며 삭제된 설정은 복원하지 않습니다. true를 유지하면 이미지 확장 기능이 비활성화될 수 있습니다.", "启动完成后在后台检查更新,并每 7 小时检查一次": "시작이 완료되면 백그라운드에서 업데이트를 확인하고 이후 7시간마다 확인합니다.", "检测到新版本,是否现在更新?": diff --git a/apps/src/lib/i18n/messages/ru.ts b/apps/src/lib/i18n/messages/ru.ts index 9daff868c..cb4c09089 100644 --- a/apps/src/lib/i18n/messages/ru.ts +++ b/apps/src/lib/i18n/messages/ru.ts @@ -184,6 +184,9 @@ export const RU_MESSAGES: MessageCatalog = { 基础设置: "Основные настройки", 控制应用启动和窗口行为: "Управление запуском приложения и поведением окна.", 自动检查更新: "Автопроверка обновлений", + "移除 requires_openai_auth": "Удалять requires_openai_auth", + "默认开启,在切换到网关或同步配置时清理旧认证标记;关闭仅保留已有值,不恢复已删除的配置。保留 true 可能禁用图片扩展。": + "По умолчанию включено: удалять старый флаг авторизации при переключении на шлюз или синхронизации профиля. Отключение сохраняет только существующие значения и не восстанавливает удалённые. Значение true может отключить расширения для изображений.", "启动完成后在后台检查更新,并每 7 小时检查一次": "Проверять обновления в фоне после запуска, а затем каждые 7 часов.", "检测到新版本,是否现在更新?": diff --git a/apps/src/lib/store/useAppStore.ts b/apps/src/lib/store/useAppStore.ts index be565f8a3..b0acb5dd2 100644 --- a/apps/src/lib/store/useAppStore.ts +++ b/apps/src/lib/store/useAppStore.ts @@ -65,6 +65,7 @@ export const useAppStore = create((set) => ({ }, appSettings: { updateAutoCheck: true, + removeRequiresOpenaiAuth: true, autoStartEnabled: false, autoStartSupported: false, showMainWindowOnStartup: true, diff --git a/apps/src/types/settings.ts b/apps/src/types/settings.ts index 4f0c4a67a..62d5ece11 100644 --- a/apps/src/types/settings.ts +++ b/apps/src/types/settings.ts @@ -51,6 +51,7 @@ export type GatewayTransportPatch = Partial; export interface AppSettings extends GatewayTransportValues { updateAutoCheck: boolean; + removeRequiresOpenaiAuth: boolean; autoStartEnabled: boolean; autoStartSupported: boolean; showMainWindowOnStartup: boolean; diff --git a/apps/tests/gateway-auth-cleanup.spec.ts b/apps/tests/gateway-auth-cleanup.spec.ts new file mode 100644 index 000000000..56bef905b --- /dev/null +++ b/apps/tests/gateway-auth-cleanup.spec.ts @@ -0,0 +1,41 @@ +import { expect, test } from "@playwright/test"; + +test("gateway auth cleanup defaults on and explicit preservation survives reload", async ({ page }) => { + let settings: Record = { + locale: "en", + codexCliGuideDismissed: true, + serviceAddr: "localhost:48760", + theme: "tech", + }; + const patches: Record[] = []; + await page.route(/\/api\/runtime\/?(?:\?.*)?$/, (route) => route.fulfill({ + json: { mode: "web-gateway", rpcBaseUrl: "/api/rpc", canManageService: false }, + })); + await page.route(/\/api\/rpc\/?(?:\?.*)?$/, async (route) => { + const { id, method, params } = route.request().postDataJSON(); + let result: unknown = {}; + if (method === "appSettings/get") result = settings; + if (method === "appSettings/set") { + patches.push(params); + settings = { ...settings, ...params }; + result = settings; + } + if (method === "initialize") result = { userAgent: "codex_cli_rs/test", codexHome: "C:/Test/.codex" }; + if (method === "accountManager/session/current") { + result = { mode: "none", role: "system_admin", permissions: [], distributionEnabled: false }; + } + await route.fulfill({ json: { jsonrpc: "2.0", id, result } }); + }); + await page.goto("/settings/"); + const control = page.getByRole("switch", { name: "Remove requires_openai_auth", exact: true }); + await expect(control).toBeChecked(); + await expect(page.locator("#settings-remove-openai-auth-description")).toContainText("may disable image extensions"); + await control.click(); + await expect.poll(() => patches.some((patch) => patch.removeRequiresOpenaiAuth === false)).toBe(true); + await page.reload(); + await expect(control).not.toBeChecked(); + await control.click(); + await expect.poll(() => patches.some((patch) => patch.removeRequiresOpenaiAuth === true)).toBe(true); + await page.reload(); + await expect(control).toBeChecked(); +}); diff --git a/crates/service/src/app_settings/api/current.rs b/crates/service/src/app_settings/api/current.rs index b94b42baa..962c6d937 100644 --- a/crates/service/src/app_settings/api/current.rs +++ b/crates/service/src/app_settings/api/current.rs @@ -27,8 +27,9 @@ use super::{ save_persisted_bool_setting, sync_runtime_settings_from_storage, APP_SETTING_AUTHOR_SERVER_RECOMMENDATIONS_KEY, APP_SETTING_AUTHOR_SPONSORS_KEY, APP_SETTING_AUTO_START_ENABLED_KEY, APP_SETTING_CLOSE_TO_TRAY_ON_CLOSE_KEY, - APP_SETTING_ENV_OVERRIDES_KEY, APP_SETTING_GATEWAY_ACCOUNT_MAX_INFLIGHT_KEY, - APP_SETTING_GATEWAY_BACKGROUND_TASKS_KEY, APP_SETTING_GATEWAY_COMPACT_MODEL_FORWARD_RULES_KEY, + APP_SETTING_CODEX_PROFILE_REMOVE_REQUIRES_OPENAI_AUTH_KEY, APP_SETTING_ENV_OVERRIDES_KEY, + APP_SETTING_GATEWAY_ACCOUNT_MAX_INFLIGHT_KEY, APP_SETTING_GATEWAY_BACKGROUND_TASKS_KEY, + APP_SETTING_GATEWAY_COMPACT_MODEL_FORWARD_RULES_KEY, APP_SETTING_GATEWAY_FREE_ACCOUNT_MAX_MODEL_KEY, APP_SETTING_GATEWAY_MODEL_FORWARD_RULES_KEY, APP_SETTING_GATEWAY_ORIGINATOR_KEY, APP_SETTING_GATEWAY_QUOTA_GUARD_KEY, APP_SETTING_GATEWAY_RESIDENCY_REQUIREMENT_KEY, APP_SETTING_GATEWAY_ROUTE_STRATEGY_KEY, @@ -158,6 +159,11 @@ fn current_app_settings_value_inner( let runtime_time_zone = current_runtime_time_zone_value(); let update_auto_check = setting_bool(&settings, APP_SETTING_UPDATE_AUTO_CHECK_KEY, true); let auto_start_enabled = setting_bool(&settings, APP_SETTING_AUTO_START_ENABLED_KEY, false); + let remove_requires_openai_auth = setting_bool( + &settings, + APP_SETTING_CODEX_PROFILE_REMOVE_REQUIRES_OPENAI_AUTH_KEY, + true, + ); let show_main_window_on_startup = setting_bool(&settings, APP_SETTING_SHOW_MAIN_WINDOW_ON_STARTUP_KEY, true); let persisted_close_to_tray = @@ -386,6 +392,10 @@ fn current_app_settings_value_inner( "webAccessPasswordConfigured": web_access_password_configured(), }); if let Some(object) = result.as_object_mut() { + object.insert( + "removeRequiresOpenaiAuth".to_string(), + remove_requires_openai_auth.into(), + ); object.insert("zoomFactor".to_string(), zoom_factor.into()); object.insert("gatewayUserAgent".to_string(), gateway_user_agent.into()); object.insert( diff --git a/crates/service/src/app_settings/api/mod.rs b/crates/service/src/app_settings/api/mod.rs index acc8b53e5..66a867318 100644 --- a/crates/service/src/app_settings/api/mod.rs +++ b/crates/service/src/app_settings/api/mod.rs @@ -49,8 +49,9 @@ pub(super) use super::{normalize_optional_text, parse_bool_with_default}; pub(super) use super::{ APP_SETTING_AUTHOR_SERVER_RECOMMENDATIONS_KEY, APP_SETTING_AUTHOR_SPONSORS_KEY, APP_SETTING_AUTO_START_ENABLED_KEY, APP_SETTING_CLOSE_TO_TRAY_ON_CLOSE_KEY, - APP_SETTING_ENV_OVERRIDES_KEY, APP_SETTING_GATEWAY_ACCOUNT_MAX_INFLIGHT_KEY, - APP_SETTING_GATEWAY_BACKGROUND_TASKS_KEY, APP_SETTING_GATEWAY_COMPACT_MODEL_FORWARD_RULES_KEY, + APP_SETTING_CODEX_PROFILE_REMOVE_REQUIRES_OPENAI_AUTH_KEY, APP_SETTING_ENV_OVERRIDES_KEY, + APP_SETTING_GATEWAY_ACCOUNT_MAX_INFLIGHT_KEY, APP_SETTING_GATEWAY_BACKGROUND_TASKS_KEY, + APP_SETTING_GATEWAY_COMPACT_MODEL_FORWARD_RULES_KEY, APP_SETTING_GATEWAY_FREE_ACCOUNT_MAX_MODEL_KEY, APP_SETTING_GATEWAY_MODEL_FORWARD_RULES_KEY, APP_SETTING_GATEWAY_ORIGINATOR_KEY, APP_SETTING_GATEWAY_QUOTA_GUARD_KEY, APP_SETTING_GATEWAY_RESIDENCY_REQUIREMENT_KEY, APP_SETTING_GATEWAY_ROUTE_STRATEGY_KEY, diff --git a/crates/service/src/app_settings/api/patch.rs b/crates/service/src/app_settings/api/patch.rs index fd30b42b3..c175c70c8 100644 --- a/crates/service/src/app_settings/api/patch.rs +++ b/crates/service/src/app_settings/api/patch.rs @@ -6,11 +6,12 @@ use super::author_links::{ normalize_author_link_items, serialize_author_link_items, AuthorLinkItem, }; use super::{ - save_persisted_app_setting, set_auto_start_enabled_setting, set_close_to_tray_on_close_setting, - set_codex_cli_guide_dismissed, set_env_overrides, set_gateway_account_max_inflight, - set_gateway_background_tasks, set_gateway_compact_model_forward_rules, - set_gateway_free_account_max_model, set_gateway_model_forward_rules, set_gateway_originator, - set_gateway_quota_guard, set_gateway_residency_requirement, set_gateway_route_strategy, + save_persisted_app_setting, save_persisted_bool_setting, set_auto_start_enabled_setting, + set_close_to_tray_on_close_setting, set_codex_cli_guide_dismissed, set_env_overrides, + set_gateway_account_max_inflight, set_gateway_background_tasks, + set_gateway_compact_model_forward_rules, set_gateway_free_account_max_model, + set_gateway_model_forward_rules, set_gateway_originator, set_gateway_quota_guard, + set_gateway_residency_requirement, set_gateway_route_strategy, set_gateway_sse_keepalive_enabled, set_gateway_sse_keepalive_interval_ms, set_gateway_thread_aware_account_distribution_enabled, set_gateway_upstream_proxy_bypass_hosts, set_gateway_upstream_proxy_url, set_gateway_upstream_stream_timeout_ms, @@ -20,13 +21,15 @@ use super::{ set_ui_appearance_preset, set_ui_locale, set_ui_low_transparency_enabled, set_ui_theme, set_ui_zoom_factor, set_update_auto_check_enabled, BackgroundTasksInput, QuotaGuardInput, APP_SETTING_AUTHOR_SERVER_RECOMMENDATIONS_KEY, APP_SETTING_AUTHOR_SPONSORS_KEY, - APP_SETTING_PLUGIN_MARKET_MODE_KEY, APP_SETTING_PLUGIN_MARKET_SOURCE_URL_KEY, + APP_SETTING_CODEX_PROFILE_REMOVE_REQUIRES_OPENAI_AUTH_KEY, APP_SETTING_PLUGIN_MARKET_MODE_KEY, + APP_SETTING_PLUGIN_MARKET_SOURCE_URL_KEY, }; #[derive(Debug, Clone, Default, Deserialize)] #[serde(rename_all = "camelCase")] pub(super) struct AppSettingsPatch { update_auto_check: Option, + remove_requires_openai_auth: Option, auto_start_enabled: Option, show_main_window_on_startup: Option, close_to_tray_on_close: Option, @@ -104,6 +107,12 @@ pub(super) fn apply_app_settings_patch(patch: AppSettingsPatch) -> Result<(), St if let Some(enabled) = patch.update_auto_check { set_update_auto_check_enabled(enabled)?; } + if let Some(enabled) = patch.remove_requires_openai_auth { + save_persisted_bool_setting( + APP_SETTING_CODEX_PROFILE_REMOVE_REQUIRES_OPENAI_AUTH_KEY, + enabled, + )?; + } if let Some(enabled) = patch.auto_start_enabled { set_auto_start_enabled_setting(enabled)?; } diff --git a/crates/service/src/app_settings/mod.rs b/crates/service/src/app_settings/mod.rs index c6962e003..1d5a07e24 100644 --- a/crates/service/src/app_settings/mod.rs +++ b/crates/service/src/app_settings/mod.rs @@ -55,6 +55,7 @@ pub(crate) use shared::{normalize_optional_text, parse_bool_with_default}; pub use shared::{ APP_SETTING_AUTHOR_SERVER_RECOMMENDATIONS_KEY, APP_SETTING_AUTHOR_SPONSORS_KEY, APP_SETTING_AUTO_START_ENABLED_KEY, APP_SETTING_CLOSE_TO_TRAY_ON_CLOSE_KEY, + APP_SETTING_CODEX_PROFILE_REMOVE_REQUIRES_OPENAI_AUTH_KEY, APP_SETTING_DISTRIBUTION_ENABLED_KEY, APP_SETTING_ENV_OVERRIDES_KEY, APP_SETTING_GATEWAY_ACCOUNT_MAX_INFLIGHT_KEY, APP_SETTING_GATEWAY_BACKGROUND_TASKS_KEY, APP_SETTING_GATEWAY_COMPACT_MODEL_FORWARD_RULES_KEY, diff --git a/crates/service/src/app_settings/shared.rs b/crates/service/src/app_settings/shared.rs index 7c4f61cb6..2badfb4a0 100644 --- a/crates/service/src/app_settings/shared.rs +++ b/crates/service/src/app_settings/shared.rs @@ -11,6 +11,8 @@ pub const APP_SETTING_UI_CODEX_CLI_GUIDE_DISMISSED_KEY: &str = "ui.codex_cli_gui pub const APP_SETTING_UI_THEME_KEY: &str = "ui.theme"; pub const APP_SETTING_UI_APPEARANCE_PRESET_KEY: &str = "ui.appearance_preset"; pub const APP_SETTING_UI_LOCALE_KEY: &str = "ui.locale"; +pub const APP_SETTING_CODEX_PROFILE_REMOVE_REQUIRES_OPENAI_AUTH_KEY: &str = + "codex_profile.remove_requires_openai_auth"; pub const APP_SETTING_SERVICE_ADDR_KEY: &str = "app.service_addr"; pub const APP_SETTING_AUTHOR_SPONSORS_KEY: &str = "author.sponsors"; pub const APP_SETTING_AUTHOR_SERVER_RECOMMENDATIONS_KEY: &str = "author.server_recommendations"; diff --git a/crates/service/src/codex_profile.rs b/crates/service/src/codex_profile.rs index 4dedebdc1..d926c4adb 100644 --- a/crates/service/src/codex_profile.rs +++ b/crates/service/src/codex_profile.rs @@ -996,6 +996,7 @@ pub(crate) async fn apply_gateway_async( &paths.gateway_model_catalog_path, supports_websockets, &secret, + remove_requires_openai_auth_enabled(), )?; write_profile_files( &profile_dir, @@ -2803,6 +2804,14 @@ fn expand_home_prefix(input: &str) -> PathBuf { PathBuf::from(input) } +fn remove_requires_openai_auth_enabled() -> bool { + crate::app_settings::get_persisted_app_setting( + crate::app_settings::APP_SETTING_CODEX_PROFILE_REMOVE_REQUIRES_OPENAI_AUTH_KEY, + ) + .map(|value| crate::app_settings::parse_bool_with_default(&value, true)) + .unwrap_or(true) +} + fn profile_key(profile_dir: &Path) -> String { profile_dir.to_string_lossy().to_string() } @@ -2982,6 +2991,7 @@ fn patch_config_for_gateway( managed_catalog_path: &Path, supports_websockets: bool, bearer_token: &str, + remove_requires_openai_auth: bool, ) -> Result { let mut doc = parse_config(content.as_deref().unwrap_or(""))?; doc.as_table_mut() @@ -3017,8 +3027,11 @@ fn patch_config_for_gateway( // Codex only treats actor authorization as an extension capability for custom providers // that do not use ambient OpenAI auth. A provider-scoped bearer token works for local and // remote CodexManager gateways without coupling the profile to a movable desktop executable. - // It is mutually exclusive with command/env/ambient auth, so remove stale values first. - provider.remove("requires_openai_auth"); + // Keep the existing actor-capable behavior by default. Users who depend on ambient auth + // can explicitly preserve their existing value, at the cost of actor/image extensions. + if remove_requires_openai_auth { + provider.remove("requires_openai_auth"); + } set_provider_bearer_auth(provider, bearer_token)?; provider.insert("base_url", toml_value(base_url)); provider.insert("wire_api", toml_value("responses")); @@ -3364,6 +3377,7 @@ async fn sync_active_gateway_profile_from_storage_with_changes_async( &paths.gateway_model_catalog_path, supports_websockets, secret, + remove_requires_openai_auth_enabled(), )?) } _ => None, diff --git a/crates/service/src/codex_profile_tests.rs b/crates/service/src/codex_profile_tests.rs index 97002f0ea..78f58b9a0 100644 --- a/crates/service/src/codex_profile_tests.rs +++ b/crates/service/src/codex_profile_tests.rs @@ -295,6 +295,7 @@ name = "Other" &managed_catalog, true, "cm-managed-key", + true, ) .expect("patch gateway"); @@ -336,6 +337,7 @@ name = "Other" &managed_catalog, false, "cm-managed-key", + true, ) .expect("disable gateway websocket"); assert!(without_websocket.contains("supports_websockets = false")); @@ -1112,6 +1114,39 @@ async fn first_apply_models_failure_removes_new_managed_state() { cleanup_profile(&dir); } +#[test] +fn gateway_config_preserves_requires_openai_auth_when_removal_is_disabled() { + let input = r#" +model_provider = "cm" + +[model_providers.cm] +name = "Custom Gateway" +requires_openai_auth = true +"#; + + let output = patch_config_for_gateway( + Some(input.to_string()), + "http://127.0.0.1:48770/v1", + Path::new("/tmp/gateway-models.json"), + false, + "cm-managed-key", + false, + ) + .expect("patch gateway without removing auth requirement"); + let doc = parse_config(&output).expect("parse patched gateway config"); + let provider = doc + .get("model_providers") + .and_then(Item::as_table) + .and_then(|providers| providers.get(PROVIDER_ID)) + .and_then(Item::as_table) + .expect("managed provider"); + + assert_eq!( + provider.get("requires_openai_auth").and_then(Item::as_bool), + Some(true) + ); +} + #[test] fn gateway_config_preserves_custom_managed_provider_values() { let input = r#" @@ -1135,6 +1170,7 @@ http_headers = { "x-existing-header" = "keep", "x-openai-actor-authorization" = &managed_catalog, true, "cm-managed-key", + true, ) .expect("patch gateway"); let doc = parse_config(&output).expect("parse patched gateway config"); @@ -1256,10 +1292,131 @@ fn invalid_toml_is_rejected() { Path::new("/tmp/gateway-models.json"), false, "cm-managed-key", + false, ) .is_err()); } +#[tokio::test(flavor = "current_thread")] +async fn gateway_auth_cleanup_setting_covers_apply_reapply_and_startup_sync() { + let _lock = crate::test_env_guard(); + let _backend_guard = EnvGuard::remove("CODEXMANAGER_STORAGE_BACKEND"); + let _database_url_guard = EnvGuard::remove("CODEXMANAGER_DATABASE_URL"); + for setting in [None, Some(false), Some(true)] { + for existing in [None, Some(false), Some(true)] { + let dir = temp_profile("gateway-auth-setting-matrix"); + let _db_guard = set_test_db(&dir); + let _managed_root_guard = EnvGuard::set( + "CODEXMANAGER_TEST_DB_DIR", + dir.join("managed").to_string_lossy().as_ref(), + ); + let storage = Storage::open(dir.join("codexmanager.db")).unwrap(); + storage.init().unwrap(); + if let Some(enabled) = setting { + storage + .set_app_setting( + crate::app_settings::APP_SETTING_CODEX_PROFILE_REMOVE_REQUIRES_OPENAI_AUTH_KEY, + if enabled { "1" } else { "0" }, + now_ts(), + ) + .unwrap(); + } + assert_eq!( + remove_requires_openai_auth_enabled(), + setting.unwrap_or(true) + ); + let key = ApiKey { + id: "auth-setting-key".into(), + name: None, + model_slug: None, + reasoning_effort: None, + service_tier: None, + rotation_strategy: crate::apikey_profile::ROTATION_HYBRID.into(), + aggregate_api_id: None, + account_plan_filter: None, + aggregate_api_url: None, + client_type: crate::apikey_profile::CLIENT_CODEX.into(), + protocol_type: crate::apikey_profile::PROTOCOL_OPENAI_COMPAT.into(), + auth_scheme: crate::apikey_profile::AUTH_BEARER.into(), + upstream_base_url: None, + static_headers_json: None, + key_hash: "auth-setting-hash".into(), + status: "active".into(), + created_at: now_ts(), + last_used_at: None, + }; + storage.insert_api_key(&key).unwrap(); + storage + .upsert_api_key_secret(&key.id, "cm-test-secret") + .unwrap(); + let auth_field = existing + .map(|value| format!("requires_openai_auth = {value}\n")) + .unwrap_or_default(); + let input = format!( + "model_provider = \"cm\"\n[model_providers.cm]\nname = \"Keep name\"\n{auth_field}\n[model_providers.other]\nrequires_openai_auth = true\nbase_url = \"https://other.example/v1\"\n" + ); + let expected = if setting.unwrap_or(true) { + None + } else { + existing + }; + let check = || { + let config = + parse_config(&fs::read_to_string(dir.join(CONFIG_FILE)).unwrap()).unwrap(); + let providers = config["model_providers"].as_table().unwrap(); + let provider = providers[PROVIDER_ID].as_table().unwrap(); + assert_eq!( + provider.get("requires_openai_auth").and_then(Item::as_bool), + expected + ); + assert_eq!(provider["name"].as_str(), Some("Keep name")); + assert_eq!( + provider["experimental_bearer_token"].as_str(), + Some("cm-test-secret") + ); + assert_eq!(provider["wire_api"].as_str(), Some("responses")); + assert_eq!( + provider_http_header( + provider, + crate::gateway::X_OPENAI_ACTOR_AUTHORIZATION_HEADER + ), + Some(crate::gateway::CODEXMANAGER_IMAGE_EXTENSION_ACTOR_AUTHORIZATION) + ); + assert_eq!( + providers["other"]["requires_openai_auth"].as_bool(), + Some(true) + ); + assert_eq!( + providers["other"]["base_url"].as_str(), + Some("https://other.example/v1") + ); + }; + fs::write(dir.join(CONFIG_FILE), &input).unwrap(); + let profile_path = dir.to_string_lossy().to_string(); + for _ in 0..2 { + apply_gateway_async( + Some(&key.id), + Some(&profile_path), + Some("http://127.0.0.1:48760"), + Some(false), + false, + ) + .await + .unwrap(); + check(); + } + // Startup sync must read the setting again instead of inheriting an old patch result. + fs::write(dir.join(CONFIG_FILE), &input).unwrap(); + assert!(sync_active_gateway_profile_from_storage_async(&storage) + .await + .unwrap()); + check(); + drop(storage); + cleanup_profile(&dir); + } + } +} + #[test] fn rotation_strategy_selects_catalog_ownership() { assert_eq!( diff --git a/crates/service/src/lib.rs b/crates/service/src/lib.rs index 395db4dad..f0123f443 100644 --- a/crates/service/src/lib.rs +++ b/crates/service/src/lib.rs @@ -144,11 +144,13 @@ pub use app_settings::{ set_show_main_window_on_startup_setting, set_ui_appearance_preset, set_ui_low_transparency_enabled, set_ui_theme, set_update_auto_check_enabled, sync_runtime_settings_from_storage, BackgroundTasksInput, APP_SETTING_AUTO_START_ENABLED_KEY, - APP_SETTING_CLOSE_TO_TRAY_ON_CLOSE_KEY, APP_SETTING_DISTRIBUTION_ENABLED_KEY, - APP_SETTING_ENV_OVERRIDES_KEY, APP_SETTING_GATEWAY_ACCOUNT_MAX_INFLIGHT_KEY, - APP_SETTING_GATEWAY_BACKGROUND_TASKS_KEY, APP_SETTING_GATEWAY_FREE_ACCOUNT_MAX_MODEL_KEY, - APP_SETTING_GATEWAY_MODEL_FORWARD_RULES_KEY, APP_SETTING_GATEWAY_ORIGINATOR_KEY, - APP_SETTING_GATEWAY_QUOTA_GUARD_KEY, APP_SETTING_GATEWAY_REQUEST_COMPRESSION_ENABLED_KEY, + APP_SETTING_CLOSE_TO_TRAY_ON_CLOSE_KEY, + APP_SETTING_CODEX_PROFILE_REMOVE_REQUIRES_OPENAI_AUTH_KEY, + APP_SETTING_DISTRIBUTION_ENABLED_KEY, APP_SETTING_ENV_OVERRIDES_KEY, + APP_SETTING_GATEWAY_ACCOUNT_MAX_INFLIGHT_KEY, APP_SETTING_GATEWAY_BACKGROUND_TASKS_KEY, + APP_SETTING_GATEWAY_FREE_ACCOUNT_MAX_MODEL_KEY, APP_SETTING_GATEWAY_MODEL_FORWARD_RULES_KEY, + APP_SETTING_GATEWAY_ORIGINATOR_KEY, APP_SETTING_GATEWAY_QUOTA_GUARD_KEY, + APP_SETTING_GATEWAY_REQUEST_COMPRESSION_ENABLED_KEY, APP_SETTING_GATEWAY_RESIDENCY_REQUIREMENT_KEY, APP_SETTING_GATEWAY_ROUTE_STRATEGY_KEY, APP_SETTING_GATEWAY_SSE_KEEPALIVE_ENABLED_KEY, APP_SETTING_GATEWAY_SSE_KEEPALIVE_INTERVAL_MS_KEY, diff --git a/crates/service/tests/app_settings.rs b/crates/service/tests/app_settings.rs index 9ea1e1d2c..e9457f3bf 100644 --- a/crates/service/tests/app_settings.rs +++ b/crates/service/tests/app_settings.rs @@ -396,6 +396,62 @@ fn app_settings_roundtrip_gateway_user_agent_and_validates_header_value() { }); } +#[test] +fn app_settings_remove_requires_openai_auth_defaults_on_and_persists_updates() { + with_temp_db(|db_path| { + let defaults = codexmanager_service::app_settings_get().expect("read default settings"); + assert_eq!(defaults["removeRequiresOpenaiAuth"], true); + let storage = Storage::open(db_path).expect("open storage"); + assert_eq!( + storage + .get_app_setting( + codexmanager_service::APP_SETTING_CODEX_PROFILE_REMOVE_REQUIRES_OPENAI_AUTH_KEY + ) + .expect("read default persisted setting"), + None + ); + drop(storage); + + let enabled = codexmanager_service::app_settings_set(Some(&json!({ + "removeRequiresOpenaiAuth": true + }))) + .expect("enable requires_openai_auth removal"); + assert_eq!(enabled["removeRequiresOpenaiAuth"], true); + + let storage = Storage::open(db_path).expect("open storage"); + assert_eq!( + storage + .get_app_setting( + codexmanager_service::APP_SETTING_CODEX_PROFILE_REMOVE_REQUIRES_OPENAI_AUTH_KEY + ) + .expect("read persisted setting") + .as_deref(), + Some("1") + ); + drop(storage); + + let disabled = codexmanager_service::app_settings_set(Some(&json!({ + "removeRequiresOpenaiAuth": false + }))) + .expect("disable requires_openai_auth removal"); + assert_eq!(disabled["removeRequiresOpenaiAuth"], false); + assert_eq!( + Storage::open(db_path) + .unwrap() + .get_app_setting( + codexmanager_service::APP_SETTING_CODEX_PROFILE_REMOVE_REQUIRES_OPENAI_AUTH_KEY + ) + .unwrap() + .as_deref(), + Some("0") + ); + assert_eq!( + codexmanager_service::app_settings_get().unwrap()["removeRequiresOpenaiAuth"], + false + ); + }); +} + #[test] fn app_settings_rejects_password_mode_without_password() { with_temp_db(|_| { diff --git a/docs/en/README.md b/docs/en/README.md index ba3199da6..ddafede76 100644 --- a/docs/en/README.md +++ b/docs/en/README.md @@ -261,6 +261,7 @@ If this project has helped you, donations are welcome! - Skills and Plugins: `/skills/` separates Skills and Codex plugins, with repository/skills.sh install, ZIP/directory import, safe uninstall, native Marketplace flow, and read-only system Skills. - Project Launcher: desktop bookmarks local folders; Windows/macOS open them in the ChatGPT Codex App, Sessions uses the local CLI, and Web/Docker do not access device directories. - Settings: manage port, listen address, proxy, timeouts, SSE keepalive, theme, updates, and background behavior. +- “Remove requires_openai_auth” in Basic settings is enabled by default, preserving the managed gateway's image/actor extension auth behavior. Disable it to preserve an existing value in the `cm` provider during gateway switching, reapplication, or startup sync; it does not restore a previously removed field. Keeping `true` may disable Codex image extensions. Direct aggregate profiles and other providers are unaffected. ### Service Edition diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index ab9d6c159..64d1d77ae 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -83,6 +83,8 @@ ## 推荐入口 ### 运行与使用 + +托管网关的“移除 requires_openai_auth”设置默认开启,以保留当前图片/actor 扩展行为。关闭后,在网关切换、重复应用与启动同步时仅保留 `cm` provider 已有值,不恢复已删除字段;保留 `true` 可能禁用 Codex 图片扩展。直连聚合 API 和其他 provider 不受此开关影响。 | 文档 | 作用 | | --- | --- | | [运行与部署指南.md](report/运行与部署指南.md) | 首次启动、Service 版、Docker、macOS 首启 |