Skip to content

fix(codex): plan a live write against the same official-ness the switch path uses - #6928

Closed
ntdatt812 wants to merge 1 commit into
farion1231:mainfrom
ntdatt812:fix/codex-official-live-write-category
Closed

ntdatt812 wants to merge 1 commit into
farion1231:mainfrom
ntdatt812:fix/codex-official-live-write-category

Conversation

@ntdatt812

Copy link
Copy Markdown
Contributor

Two definitions of "official", and the live write uses the weaker one

plan_codex_live_write decides official-ness from one thing:

if category == Some("official") {

The rest of the switch path asks a different question. services/provider/mod.rs — including the block that cleans up auth.json after the very write this plan produces — uses:

provider.category.as_deref() == Some("official")
    || crate::proxy::providers::is_codex_official_provider(provider)

And is_codex_official_provider is broader on purpose. It returns true for the fixed official row, and it returns true for any provider bound to a managed codex_oauth account, regardless of what category holds:

if has_managed_account {
    return true;
}

So a card that every other part of the switch treats as official can reach the plan and be judged third-party.

What that costs

The third-party branch does two things the official branch explicitly does not:

official branch third-party branch
write_full_auth codex_auth_has_login_material(auth) false
remove_auth_file false !preserve_official_login

Preservation is opt-in and defaults to false, so for such a card:

  1. its own ChatGPT login, sitting in auth, is never written to auth.json — the plan is config-only; and
  2. the live auth.json is deleted.

Switching to your own official card signs you out. Then services/provider/mod.rs runs clear_stale_codex_live_auth_after_official_switch — which does use the broad predicate — finds no file, and returns Ok(false). Nothing is logged, because from that block's point of view there was nothing stale to clean.

record_codex_managed_oauth_live_auth, which runs right after the write, does not rescue it: it writes the ownership marker file, not auth.json.

The fix

The evidence that this is a known-shaped problem is already in the tree — one managed-account path in services/proxy.rs hardcodes the category:

// An explicitly managed official account is different from the
// unbound native-login passthrough: the selected account owns
// auth.json and must replace any previously active account.
crate::codex_config::write_codex_live_for_provider(Some("official"), auth, ...)

That is the right call made in one place. This PR names the rule once instead:

pub fn codex_live_write_category(provider: &Provider) -> Option<&str> {
    if crate::proxy::providers::is_codex_official_provider(provider) {
        return Some("official");
    }
    provider.category.as_deref()
}

and routes the five sites that build a Codex live write through it — services/config.rs, services/provider/live.rs (both the preflight and the write), and the two services/proxy.rs sites.

Including the preflight is the part I would not want dropped. plan_codex_live_write's own doc says validation and execution live in one builder so they cannot drift; that guarantee is only worth anything if both are handed the same provider identity. A preflight that judged a card third-party while the write judged it official would refuse switches that would have succeeded, or pass ones that then behave differently.

I deliberately did not touch is_codex_official_provider itself. Widening the predicate would change proxy routing and base-URL resolution, which is a much larger blast radius than the write-planning bug actually needs.

Verification

Two tests, and the first is red against main.

thread 'codex_config::tests::a_managed_oauth_card_is_official_even_when_its_category_is_not'
panicked at src\codex_config.rs:5715:9:
planning an official card as third-party deletes the ChatGPT login it switches TO

The harm is asserted before the mechanism on purpose — with the assertions the other way round, the failure reads "expected Some("official"), got Some("codex")", which says what a helper returned rather than what breaks for the user. It also opens with a precondition assert that is_codex_official_provider already calls the fixture official, so the test cannot quietly become vacuous if that predicate changes.

The second test pins the other direction: a genuine third-party card keeps its own category, still deletes auth.json with preservation off, and stays config-only. Its fixture carries a real [model_providers.relay] table — my first attempt used an empty config, which is refused outright (provider.codex.config.missing) and would have passed for the wrong reason.

cargo test --lib: 2713 passed, 2 failed. Both failures are pre-existing and environmental — resolve_catalog_rejects_symlink_escaping_config_dir and symlink_cycle_does_not_cause_stack_overflow both die on Os { code: 1314, "A required privilege is not held by the client." }, this Windows box having no symlink privilege. Neither is in a file this branch touches.

cargo clippy --all-targets -- -D warnings → clean. cargo fmt --all applied.

Scope note

This is not a fix for #6925, which reports the reverse direction (official → third-party with preservation on). I went looking for that one and could not reproduce it in the plan — remove_auth_file = !preserve_official_login is correct there and covered by existing tests — so I am not claiming it. This is a different reachable path to the same symptom, found on the way.

@farion1231

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-08-29T02:05:11.624812Z d368164 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: d368164747

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…ch path uses

plan_codex_live_write decided official-ness from provider.category alone. The
rest of the switch path — including its own post-switch auth.json cleanup —
asks is_codex_official_provider, which also recognises the fixed official row
and any provider bound to a managed codex_oauth account, whatever their stored
category says.

A card official to the predicate but not to the string was planned as a
third-party switch: write_full_auth stayed false, so its ChatGPT login never
reached auth.json, and remove_auth_file became !preserve_official_login — true
by default — so the live login was deleted. Switching to your own official card
signed you out, and the post-switch cleanup, which does use the predicate, then
found nothing left to clean.

One managed-account path in services::proxy already hardcoded Some("official")
to sidestep this. codex_live_write_category names the rule once, and both the
preflight and the write now read it, so the two cannot disagree about which
provider they are judging.
@ntdatt812
ntdatt812 force-pushed the fix/codex-official-live-write-category branch from d368164 to 13f1c06 Compare September 1, 2026 04:34
@funkpopo

Copy link
Copy Markdown
Collaborator

@ntdatt812 感谢您的提交,「写入计划必须和切换路径的其他部分用同一个 official-ness 定义」这个不变量意识非常好——两套判定并存正是会出这类 bug 的地方!

这个 PR 的目标已经在 main 中实现,将以此为依据关闭本 PR。

main 中的对应实现

PR 针对的 plan_codex_live_write(旧的整份写入计划)在 main 中已不存在——Codex 写入路径重构为补丁式写入(src-tauri/src/services/provider/codex_direct.rs)。重构后的官方判定是统一的宽定义(codex_direct.rs 的 is_official,第 62–64 行):

pub(crate) fn is_official(provider: &Provider) -> bool {
    provider.category.as_deref() == Some("official")
        || crate::proxy::providers::is_codex_official_provider(provider)
}

模块头注释明确「官方卡:category == "official",或按 is_codex_official_provider 认出来的(早期……)」。切换、Stack 目录(prepare_official_rows 等)、写入全部走这一个判定,不再有「其他部分当官方、写入计划当第三方」的分裂。

您的工作没有白做

「两套 official-ness 定义并存会出 bug」的分析完全正确——main 的重构正是把判定收敛成一处;绑定托管 codex_oauth 账号即视为官方(has_managed_account → true)的语义也一致保留在 is_codex_official_provider 里。

如果您在最新 main 上复现出「绑定托管账号的卡片被写入路径当第三方」的场景,欢迎开 issue 附复现步骤,我们按场景处理。

我将关闭这个 PR,再次感谢您的时间和耐心!

@funkpopo funkpopo closed this Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Backend (Rust/Tauri)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants