Skip to content

Commit 66cfe54

Browse files
committed
fix(codex): profile auth files
Treat Codex auth.json as opaque provider state. Save and restore whole-file auth profiles while keeping config.toml on minimal toml_edit patches. Refs #2
1 parent 88190b0 commit 66cfe54

5 files changed

Lines changed: 364 additions & 45 deletions

File tree

AGENTS.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ ux.rs — prompt / diff / 简单表格输出
2828
|------|------|
2929
| `~/.config/cxf/base.toml` | 默认 model/review_model 设置(XDG_CONFIG_HOME) |
3030
| `~/.config/cxf/providers/` | 每个 Codex provider 一个 `.toml` 文件 |
31+
| `~/.config/cxf/auth/codex/` | 每个 Codex provider 的完整 `auth.json` profile |
3132
| `~/.config/cxf/claude/providers/` | 每个 Claude provider 一个 `.toml` 文件 |
32-
| `~/.local/state/cxf/snapshots/` | 切换前自动备份(XDG_STATE_HOME) |
33+
| `~/.local/state/cxf/snapshots/` | 切换前事故备份(XDG_STATE_HOME) |
3334
| `~/.codex/config.toml` | Codex 主配置(cxf 注入 `# cxf: provider = <name>` 标记) |
34-
| `~/.codex/auth.json` | Codex API key 存储 |
35+
| `~/.codex/auth.json` | Codex 认证状态;cxf 按 provider 做 whole-file profile 保存/恢复 |
3536
| `~/.claude/settings.json` | Claude Code 配置(cxf 只改 managed env + 顶层 model) |
3637

3738
## 硬规则
@@ -52,7 +53,7 @@ ux.rs — prompt / diff / 简单表格输出
5253
- `model_context_window`, `model_auto_compact_token_limit`
5354
- `[model_providers.<name>]`
5455
- `[features].responses_websockets_v2`
55-
- `auth.json` `OPENAI_API_KEY`
56+
- `auth.json` whole-file provider profile(不解析 OAuth 字段;API-key provider 可由 `api_key` 生成最小 auth)
5657
- 其他 Codex 配置必须保留。
5758
- 用户当前倾向:`model_providers` 一般保持 `"OpenAI"`,避免 Codex 历史按 provider key 分裂;如要改,必须明确知道风险。
5859

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,17 @@ cxf claude status
132132
├── providers/ # 每个 provider 一个 .toml 文件
133133
│ ├── openai.toml
134134
│ └── ...
135+
├── auth/
136+
│ └── codex/ # 每个 Codex provider 的完整 auth.json profile
137+
│ ├── official.json
138+
│ └── ...
135139
└── claude/
136140
└── providers/ # Claude provider 定义
137141
├── deepseek.toml
138142
└── ...
139143
140144
~/.local/state/cxf/ # XDG_STATE_HOME → cxf 运行状态
141-
└── snapshots/ # 切换前自动备份
145+
└── snapshots/ # 切换前事故备份
142146
└── ...
143147
```
144148

@@ -148,7 +152,7 @@ cxf 只操作以下 Codex 配置字段:
148152
- `model_context_window`, `model_auto_compact_token_limit`
149153
- `[model_providers.<name>]`
150154
- `[features].responses_websockets_v2`
151-
- `auth.json` 中的 `OPENAI_API_KEY`
155+
- `auth.json` 作为不透明状态文件按 provider 完整保存/恢复;API-key provider 首次使用时从 `api_key` 生成最小 auth profile
152156

153157
其他所有 Codex 配置保持不动。
154158

src/codex.rs

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ use toml_edit::{DocumentMut, Item, table, value};
77
use crate::{
88
config::{
99
BASE_KEYS, auth_path, base_path, codex_config_path, ensure_layout, load_base,
10-
providers_dir, read_auth, read_text, read_toml, take_snapshot, write_auth,
11-
write_default_base, write_secret, write_toml,
10+
providers_dir, read_auth, read_text, read_toml, restore_auth_profile, save_auth_profile,
11+
take_snapshot, write_auth, write_default_base, write_secret, write_toml,
1212
},
1313
models::{
1414
PROBE_PREFIX, Provider, ensure_provider_id, get_bool, get_i64, get_str,
@@ -313,9 +313,30 @@ pub fn extract_all_providers(current_name: Option<&str>) -> Result<Vec<Provider>
313313
pub fn cmd_init(name: Option<&str>) -> Result<()> {
314314
ensure_layout()?;
315315
write_default_base()?;
316+
let config_path = codex_config_path();
317+
let before_config = read_text(&config_path)?;
318+
let current_model_provider = get_str(&read_toml(&config_path)?, "model_provider")
319+
.unwrap_or_else(|| "OpenAI".to_string());
316320
let providers = extract_all_providers(name)?;
321+
let mut current_provider_id = None;
317322
for provider in &providers {
318323
write_provider(provider)?;
324+
if provider.model_providers == current_model_provider {
325+
save_auth_profile(&provider.provider_id)?;
326+
current_provider_id = Some(provider.provider_id.as_str());
327+
}
328+
}
329+
if let Some(provider_id) = current_provider_id {
330+
take_snapshot(
331+
&config_path,
332+
"codex-config",
333+
&read_provider_probe(&before_config),
334+
"toml",
335+
)?;
336+
write_secret(
337+
&config_path,
338+
set_provider_probe(&before_config, provider_id).as_bytes(),
339+
)?;
319340
}
320341
println!("initialized: {}", providers_dir().display());
321342
for provider in providers {
@@ -350,24 +371,28 @@ pub fn cmd_use(provider_id: &str) -> Result<()> {
350371
} else {
351372
String::new()
352373
};
353-
take_snapshot(
354-
&config_path,
355-
"codex-config",
356-
&read_provider_probe(&before_config),
357-
"toml",
358-
)?;
374+
let previous_provider = read_provider_probe(&before_config);
375+
save_auth_profile(&previous_provider)?;
376+
take_snapshot(&config_path, "codex-config", &previous_provider, "toml")?;
359377
let config = read_toml(&config_path)?;
360378
let after_doc = apply_provider(config, &base, &provider)?;
361379
let after_config = set_provider_probe(&after_doc.to_string(), &provider.provider_id);
362380
write_secret(&config_path, after_config.as_bytes())?;
363-
if !provider.api_key.is_empty() {
364-
take_snapshot(
365-
&auth_path,
366-
"codex-auth",
367-
&read_provider_probe(&before_config),
368-
"json",
369-
)?;
370-
write_auth(&provider.api_key)?;
381+
let restored = restore_auth_profile(&provider.provider_id)?;
382+
if provider.api_key.is_empty() {
383+
if !restored {
384+
eprintln!(
385+
"{} warning: no Codex auth profile for {}, leaving auth.json unchanged",
386+
yellow("!"),
387+
provider.provider_id
388+
);
389+
}
390+
} else {
391+
let auth = read_auth()?;
392+
if auth.get("OPENAI_API_KEY").and_then(Value::as_str) != Some(provider.api_key.as_str()) {
393+
write_auth(&provider.api_key)?;
394+
save_auth_profile(&provider.provider_id)?;
395+
}
371396
}
372397
let after_auth = if auth_path.exists() {
373398
read_text(&auth_path)?

src/config.rs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ pub fn claude_providers_dir() -> PathBuf {
4848
pub fn snapshots_dir() -> PathBuf {
4949
state_home().join("cxf/snapshots")
5050
}
51+
pub fn auth_profiles_dir() -> PathBuf {
52+
cxf_home().join("auth/codex")
53+
}
5154
pub fn codex_config_path() -> PathBuf {
5255
home_dir().join(".codex/config.toml")
5356
}
@@ -60,6 +63,7 @@ pub fn claude_settings_path() -> PathBuf {
6063

6164
pub fn ensure_layout() -> Result<()> {
6265
fs::create_dir_all(providers_dir()).context("create providers dir")?;
66+
fs::create_dir_all(auth_profiles_dir()).context("create codex auth profiles dir")?;
6367
fs::create_dir_all(snapshots_dir()).context("create snapshots dir")?;
6468
Ok(())
6569
}
@@ -137,6 +141,7 @@ pub fn write_secret(path: &Path, data: &[u8]) -> Result<()> {
137141
.with_context(|| format!("create {}", path.display()))?;
138142
f.write_all(data)
139143
.with_context(|| format!("write {}", path.display()))?;
144+
chmod_600(path)?;
140145
}
141146
#[cfg(not(unix))]
142147
{
@@ -213,14 +218,57 @@ pub fn write_default_base() -> Result<()> {
213218
write_toml(&path, &doc)
214219
}
215220

221+
fn safe_provider_file(provider: &str) -> String {
222+
provider.replace(['/', '\\'], "_")
223+
}
224+
225+
fn snapshot_path(prefix: &str, provider: &str, ext: &str) -> PathBuf {
226+
snapshots_dir().join(format!("{prefix}-{}.{ext}", safe_provider_file(provider)))
227+
}
228+
229+
fn auth_profile_path(provider: &str) -> PathBuf {
230+
auth_profiles_dir().join(format!("{}.json", safe_provider_file(provider)))
231+
}
232+
216233
pub fn take_snapshot(source: &Path, prefix: &str, provider: &str, ext: &str) -> Result<()> {
217234
if !source.exists() {
218235
return Ok(());
219236
}
220237
fs::create_dir_all(snapshots_dir()).context("create snapshots dir")?;
221-
let safe = provider.replace(['/', '\\'], "_");
222-
let target = snapshots_dir().join(format!("{prefix}-{safe}.{ext}"));
238+
let target = snapshot_path(prefix, provider, ext);
223239
fs::copy(source, &target).context("write snapshot")?;
224240
chmod_600(&target)?;
225241
Ok(())
226242
}
243+
244+
pub fn save_auth_profile(provider: &str) -> Result<bool> {
245+
if provider.trim().is_empty() || !auth_path().exists() {
246+
return Ok(false);
247+
}
248+
read_auth()?;
249+
fs::create_dir_all(auth_profiles_dir()).context("create codex auth profiles dir")?;
250+
let text = read_text(&auth_path())?;
251+
write_secret(&auth_profile_path(provider), text.as_bytes())?;
252+
Ok(true)
253+
}
254+
255+
pub fn restore_auth_profile(provider: &str) -> Result<bool> {
256+
if provider.trim().is_empty() {
257+
return Ok(false);
258+
}
259+
let profile = auth_profile_path(provider);
260+
if !profile.exists() {
261+
let legacy_snapshot = snapshot_path("codex-auth", provider, "json");
262+
if !legacy_snapshot.exists() {
263+
return Ok(false);
264+
}
265+
read_json(&legacy_snapshot)?;
266+
fs::create_dir_all(auth_profiles_dir()).context("create codex auth profiles dir")?;
267+
fs::copy(&legacy_snapshot, &profile).context("import legacy auth snapshot")?;
268+
chmod_600(&profile)?;
269+
}
270+
read_json(&profile)?;
271+
let text = read_text(&profile)?;
272+
write_secret(&auth_path(), text.as_bytes())?;
273+
Ok(true)
274+
}

0 commit comments

Comments
 (0)