Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions apps/codex-plus-launcher/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,16 @@ impl LaunchHooks for LauncherHooks {
self.core.ensure_computer_use_config(settings).await
}

async fn ensure_builtin_plugin_state_after_provider_switch(
&self,
settings: &codex_plus_core::settings::BackendSettings,
source: &str,
) -> anyhow::Result<()> {
self.core
.ensure_builtin_plugin_state_after_provider_switch(settings, source)
.await
}

async fn ensure_plugin_marketplace_config(
&self,
settings: &codex_plus_core::settings::BackendSettings,
Expand Down Expand Up @@ -819,6 +829,10 @@ mod tests {

assert!(source.contains("async fn ensure_computer_use_config"));
assert!(source.contains("self.core.ensure_computer_use_config(settings).await"));
assert!(source.contains("async fn ensure_builtin_plugin_state_after_provider_switch"));
assert!(
source.contains(".ensure_builtin_plugin_state_after_provider_switch(settings, source)")
);
assert!(source.contains("async fn ensure_plugin_marketplace_config"));
assert!(source.contains("self.core.ensure_plugin_marketplace_config(settings).await"));
assert!(source.contains("async fn start_computer_use_guard_watchdog"));
Expand Down
206 changes: 203 additions & 3 deletions apps/codex-plus-manager/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2505,6 +2505,7 @@ fn provider_doctor_recommendation(checks: &[ProviderDoctorCheck]) -> String {
pub fn apply_relay_injection() -> CommandResult<RelayPayload> {
let home = codex_plus_core::relay_config::default_codex_home_dir();
let settings = SettingsStore::default().load().unwrap_or_default();
prepare_codex_app_state_before_provider_switch(&home, "manager.apply_relay_injection.before");
if !settings.relay_profiles_enabled {
let status = codex_plus_core::relay_config::relay_status_from_home(&home);
return failed(
Expand All @@ -2515,16 +2516,29 @@ pub fn apply_relay_injection() -> CommandResult<RelayPayload> {
let relay = settings.active_relay_profile();
log_relay_apply_request("manager.apply_relay_injection", &settings, &relay);
if settings.active_aggregate_relay_profile().is_some() {
return apply_aggregate_relay_injection_to_home(&home);
let result = apply_aggregate_relay_injection_to_home(&home);
if result.status == "ok" {
finish_codex_app_state_after_provider_switch(
&home,
&settings,
"manager.apply_relay_injection.aggregate",
);
}
return result;
}
if relay_has_complete_files(&relay) {
return match codex_plus_core::relay_config::apply_relay_profile_to_home_with_switch_rules_and_computer_use_guard(
&home,
&relay,
&relay_combined_common_config(&settings),
settings.computer_use_guard_enabled,
settings.builtin_plugin_guard_enabled(),
) {
Ok(result) => {
finish_codex_app_state_after_provider_switch(
&home,
&settings,
"manager.apply_relay_injection.profile",
);
let status = codex_plus_core::relay_config::relay_status_from_home(&home);
log_relay_apply_result(
"manager.apply_relay_injection.ok",
Expand Down Expand Up @@ -2579,6 +2593,11 @@ pub fn apply_relay_injection() -> CommandResult<RelayPayload> {
codex_plus_core::protocol_proxy::DEFAULT_PROTOCOL_PROXY_PORT,
) {
Ok(result) => {
finish_codex_app_state_after_provider_switch(
&home,
&settings,
"manager.apply_relay_injection.generated",
);
let status = codex_plus_core::relay_config::relay_status_from_home(&home);
log_relay_apply_result(
"manager.apply_relay_injection.ok",
Expand Down Expand Up @@ -2640,6 +2659,10 @@ fn apply_aggregate_relay_injection_to_home(home: &Path) -> CommandResult<RelayPa
pub fn apply_pure_api_injection() -> CommandResult<RelayPayload> {
let home = codex_plus_core::relay_config::default_codex_home_dir();
let settings = SettingsStore::default().load().unwrap_or_default();
prepare_codex_app_state_before_provider_switch(
&home,
"manager.apply_pure_api_injection.before",
);
if !settings.relay_profiles_enabled {
let status = codex_plus_core::relay_config::relay_status_from_home(&home);
return failed(
Expand All @@ -2654,9 +2677,14 @@ pub fn apply_pure_api_injection() -> CommandResult<RelayPayload> {
&home,
&relay,
&relay_combined_common_config(&settings),
settings.computer_use_guard_enabled,
settings.builtin_plugin_guard_enabled(),
) {
Ok(result) => {
finish_codex_app_state_after_provider_switch(
&home,
&settings,
"manager.apply_pure_api_injection.profile",
);
let status = codex_plus_core::relay_config::relay_status_from_home(&home);
log_relay_apply_result(
"manager.apply_pure_api_injection.ok",
Expand Down Expand Up @@ -2701,6 +2729,11 @@ pub fn apply_pure_api_injection() -> CommandResult<RelayPayload> {
codex_plus_core::protocol_proxy::DEFAULT_PROTOCOL_PROXY_PORT,
) {
Ok(result) => {
finish_codex_app_state_after_provider_switch(
&home,
&settings,
"manager.apply_pure_api_injection.generated",
);
let status = codex_plus_core::relay_config::relay_status_from_home(&home);
log_relay_apply_result(
"manager.apply_pure_api_injection.ok",
Expand Down Expand Up @@ -2743,13 +2776,19 @@ pub fn clear_relay_injection() -> CommandResult<RelayPayload> {
let settings = SettingsStore::default().load().unwrap_or_default();
let relay = settings.active_relay_profile();
log_manager_event("manager.clear_relay_injection.start", json!({}));
prepare_codex_app_state_before_provider_switch(&home, "manager.clear_relay_injection.before");
let auth_contents = (relay.relay_mode == codex_plus_core::settings::RelayMode::Official
&& !relay.official_mix_api_key
&& !relay.auth_contents.trim().is_empty())
.then_some(relay.auth_contents.as_str());
match codex_plus_core::relay_config::clear_relay_config_to_home_with_auth(&home, auth_contents)
{
Ok(result) => {
finish_codex_app_state_after_provider_switch(
&home,
&settings,
"manager.clear_relay_injection.after",
);
let status = codex_plus_core::relay_config::relay_status_from_home(&home);
log_manager_event(
"manager.clear_relay_injection.ok",
Expand Down Expand Up @@ -2780,6 +2819,50 @@ pub fn clear_relay_injection() -> CommandResult<RelayPayload> {
}
}

fn prepare_codex_app_state_before_provider_switch(home: &Path, source: &str) {
codex_plus_core::codex_app_state::capture_app_state_snapshot_nonfatal(home, source);
}

fn finish_codex_app_state_after_provider_switch(
home: &Path,
settings: &BackendSettings,
source: &str,
) {
if settings.codex_app_plugin_marketplace_unlock {
match codex_plus_core::plugin_marketplace::ensure_openai_curated_remote_marketplace_available(
home,
) {
Ok(result) => {
if result.initialized || result.configured {
log_manager_event(
"manager.remote_plugin_marketplace_ready",
json!({
"source": source,
"initialized": result.initialized,
"configured": result.configured,
}),
);
}
}
Err(error) => {
log_manager_event(
"manager.remote_plugin_marketplace_failed",
json!({
"source": source,
"error": error.to_string(),
}),
);
}
}
}
if settings.builtin_plugin_guard_enabled() {
codex_plus_core::codex_app_state::ensure_builtin_plugin_state_after_provider_switch_nonfatal(
home, source,
);
}
codex_plus_core::codex_app_state::sync_app_state_after_provider_switch_nonfatal(home, source);
}

fn relay_has_complete_files(relay: &codex_plus_core::settings::RelayProfile) -> bool {
if relay.relay_mode == codex_plus_core::settings::RelayMode::Official
&& relay.official_mix_api_key
Expand Down Expand Up @@ -3781,6 +3864,123 @@ mod tests {
assert_eq!(result.payload.settings.relay_profiles[0].api_key, "sk-test");
}

#[test]
fn provider_switch_state_helpers_restore_allowed_app_state() {
let temp = tempfile::tempdir().unwrap();
let home = temp.path().join("codex-home");
std::fs::create_dir(&home).unwrap();
let state_path = home.join(".codex-global-state.json");
std::fs::write(
&state_path,
json!({
"electron-saved-workspace-roots": ["C:/work/app"],
"thread-workspace-root-hints": {
"thread-1": "C:/work/app"
},
"electron-persisted-atom-state": {
"default-service-tier": "priority",
"plugin-marketplace-unlocked": true,
"prompt-history": ["secret"]
},
"computer-use-bundled-plugin-auto-install-disabled": true,
"prompt-history": ["secret"]
})
.to_string(),
)
.unwrap();
#[cfg(windows)]
{
let marketplace = home
.join(".tmp")
.join("bundled-marketplaces")
.join("openai-bundled");
std::fs::create_dir_all(marketplace.join(".agents").join("plugins")).unwrap();
std::fs::write(
marketplace
.join(".agents")
.join("plugins")
.join("marketplace.json"),
"{}",
)
.unwrap();
for plugin in ["browser", "chrome", "computer-use", "latex"] {
let plugin_root = marketplace
.join("plugins")
.join(plugin)
.join(".codex-plugin");
std::fs::create_dir_all(&plugin_root).unwrap();
std::fs::write(plugin_root.join("plugin.json"), "{}").unwrap();
}
}
prepare_codex_app_state_before_provider_switch(&home, "test.before");
std::fs::write(
&state_path,
json!({
"electron-saved-workspace-roots": ["D:/fresh/app"],
"computer-use-bundled-plugin-auto-install-disabled": true
})
.to_string(),
)
.unwrap();
let settings = BackendSettings {
codex_app_plugin_marketplace_unlock: false,
computer_use_guard_enabled: true,
..BackendSettings::default()
};

finish_codex_app_state_after_provider_switch(&home, &settings, "test.after");

let state: Value =
serde_json::from_str(&std::fs::read_to_string(&state_path).unwrap()).unwrap();
assert_eq!(
state["electron-saved-workspace-roots"],
json!(["D:\\fresh\\app", "C:\\work\\app"])
);
assert_eq!(
state["thread-workspace-root-hints"]["thread-1"],
"C:/work/app"
);
assert_eq!(
state["electron-persisted-atom-state"]["default-service-tier"],
"priority"
);
assert_eq!(
state["electron-persisted-atom-state"]["plugin-marketplace-unlocked"],
true
);
assert_eq!(
state["computer-use-bundled-plugin-auto-install-disabled"],
false
);
assert!(state.get("prompt-history").is_none());
assert!(
state["electron-persisted-atom-state"]
.get("prompt-history")
.is_none()
);
assert!(
home.join("backups_state/app-state-sync/latest-safe-state.json")
.is_file()
);
#[cfg(windows)]
{
let config = std::fs::read_to_string(home.join("config.toml")).unwrap();
assert!(config.contains("[marketplaces.openai-bundled]"));
assert!(config.contains("[plugins.\"browser@openai-bundled\"]"));
assert!(config.contains("[plugins.\"chrome@openai-bundled\"]"));
assert!(config.contains("[plugins.\"computer-use@openai-bundled\"]"));
assert!(config.contains("computer_use = true"));
assert!(config.contains("sandbox = \"unelevated\""));
}
let backup_root = home.join("backups_state/app-state-sync");
let backup_count = std::fs::read_dir(&backup_root)
.unwrap()
.filter_map(Result::ok)
.filter(|entry| entry.path().join(".codex-global-state.json").is_file())
.count();
assert_eq!(backup_count, 1);
}

#[test]
fn normalize_settings_before_save_preserves_official_profile_auth() {
let settings = BackendSettings {
Expand Down
4 changes: 2 additions & 2 deletions apps/codex-plus-manager/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2650,8 +2650,8 @@ function EnhanceScreen({
type="checkbox"
/>
<span>
<strong>{t("启用 Windows Computer Use Guard")}</strong>
<small>{t("默认关闭;开启后启动 Codex 时会自动保留官方 Computer Use 插件所需的 config.toml、bundled 插件和 notify 配置。")}</small>
<strong>{t("强制启用 Windows Computer Use Guard")}</strong>
<small>{t("插件市场解锁在完整增强模式下会自动准备 Browser、Chrome、Computer Use 的本地 bundled 状态;此开关用于在其他场景中强制执行同一守护。")}</small>
</span>
</label>
<ModeSelector launchMode={form.launchMode} actions={actions} />
Expand Down
6 changes: 3 additions & 3 deletions apps/codex-plus-manager/src/i18n-en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export const EN_PLAIN: Record<string, string> = {
"启用": "Enable",
"启用 Codex 图片覆盖层": "Enable Codex image overlay",
"启用 Codex增强": "Enable Codex enhancements",
"启用 Windows Computer Use Guard": "Enable Windows Computer Use Guard",
"强制启用 Windows Computer Use Guard": "Force-enable Windows Computer Use Guard",
"启用供应商配置切换": "Enable provider configuration switching",
"启用此扩展项": "Enable this entry",
"启用目标功能": "Enable goals feature",
Expand Down Expand Up @@ -631,8 +631,8 @@ export const EN_PLAIN: Record<string, string> = {
"额外参数": "Extra arguments",
"高级选项,默认关闭;当前实现不主动改写 Zed settings。": "Advanced option, off by default; the current implementation doesn't actively modify Zed settings.",
"默认中转": "Default relay",
"默认关闭;开启后启动 Codex 时会自动保留官方 Computer Use 插件所需的 config.toml、bundled 插件和 notify 配置。":
"Off by default; when on, launching Codex automatically preserves the config.toml, bundled plugins and notify config required by the official Computer Use plugin.",
"插件市场解锁在完整增强模式下会自动准备 Browser、Chrome、Computer Use 的本地 bundled 状态;此开关用于在其他场景中强制执行同一守护。":
"Plugin marketplace unlock automatically prepares local bundled Browser, Chrome and Computer Use state in full enhancement mode; this switch force-runs the same guard in other scenarios.",
"默认启动 Codex 时使用的模型名,请勿带后缀;上下文窗口请在下方「模型列表」中按模型单独配置。":
"The model name used by default when launching Codex; don't include a suffix. Configure context windows per model in the “Model list” below.",
"默认关闭;无 VPN 时可开启,让 Statsig 初始化快速失败,减少启动时长。需重启 Codex 才生效。":
Expand Down
Loading
Loading