B5|配置、鉴权、持久化:身份在 CODEX_HOME,项目层默认不可信
先别把 config.toml 当成一份文件
读完 B3、B4,下一件错事是打开 ~/.codex/config.toml,以为沙箱、模型和登录都写在这一个表里。然后你会把仓库里的 .codex/config.toml 当成「项目覆盖」,把 auth.json 当成「另一份配置」,把 rollout 和 sqlite 当成「同一种数据库」。
判断先说清楚:有效配置是一层一层折出来的;登录态和会话日志住在 CODEX_HOME;项目目录默认不可信。 克隆一个带 .codex/ 的仓库,不能改你的账号、不能改你的审批默认值、不能把 hook 和 execpolicy 偷偷跑起来。
A1 已经把 ChatGPT 账号和 API Key 的 能力差 钉死。这一章问三件更底层的事:配置从哪几层来、信任开关切掉什么、磁盘上到底有几份持久化。
1. CODEX_HOME 是身份根
find_codex_home(utils/home-dir/src/lib.rs:5-17):环境变量 CODEX_HOME 若设置,必须已经是一个目录,否则 fatal,并 canonicalize。未设置则默认 ~/.codex,此时不要求目录已存在。测试可以指向临时目录,生产路径一旦写错就该硬失败,不许静默掉回 home。
这棵目录不是「一个 toml 的文件夹」,是本机 Codex 身份:
| 住在这里 | 是什么 |
|---|---|
config.toml / <profile>.config.toml | 用户层配置 |
auth.json 或 keyring | 登录态 |
sessions/、archived_sessions/ | rollout JSONL(B4) |
| sqlite state | 线程索引、元数据、持久队列(B1) |
environments.toml | 执行后端发现(B6);没有则本机 local |
memories/、plugins/、skills/ | 跨会话资产(C / D) |
history.jsonl | 全局、append-only 的输入框历史(message-history crate),一行一条 {session_id, ts, text};只给 composer 上翻用,不是 rollout,也不是 C3 的 Chronicle |
六个入口(A1)共用这一棵树,才谈得上同一账号、同一历史。SDK 文档也强调:额外 --config 不应偷偷改 CODEX_HOME。身份根和一次 CLI 覆盖不是一回事。
没有独立的 CODEX_HOME 会怎样?测试会污染你的真实登录;多版本 Codex 抢同一份 auth.json;resume 找不到会话却去 PWD 里翻 .codex。配置可以按项目叠,账号和历史必须按用户。
2. 分层合并:后者覆盖前者,且能查出每个 key 从哪来
权威说明在 config/src/loader/README.md 和 loader/mod.rs:114-126。折的时候 overlay 赢(merge.rs:57-59)。对外的优先级是 上压下:
七层不是等权 merge:越往上(数字越小)优先级越高,能覆盖下面所有层,但不代表越上层就越安全——下面马上会讲,就算是最高优先级的 SessionFlags / Project 层,也进不了 denylist 挡住的那批凭证键。
- MDM / legacy managed(企业设备策略,正在淘汰直写
managed_config.toml) - SessionFlags:
--config、CLI 旋钮 - Project:
.codex/config.toml(以及 cwd / 祖先 / git root 上的同类文件) - User profile:
${CODEX_HOME}/<name>.config.toml - User:
${CODEX_HOME}/config.toml - EnterpriseManaged 云端托管包
- System:
/etc/codex/config.toml或 Windows%ProgramData%\OpenAI\Codex\config.toml
内部存储顺序相反:低优先级在前,折叠时后面覆盖前面。ConfigLayerStack 同时给出 effective_config() 和 origins()——每个 key 能指出是哪一层赢的。企业冲突检测靠 per-layer fingerprint。
EnterpriseManaged 层不是本机现算的,是下载下来的。cloud-config crate 拉取 ConfigBundleResponse(DeliveredConfigToml / DeliveredRequirementsToml / DeliveredTomlFragment,均来自 codex-backend-client,cloud-config/src/backend.rs:1-3),再落进这一层。托管配置包和 EnterpriseManaged config 是同一份数据的两种叫法,客户端不本地拼装规则,只信后端签发的那份 bundle。
即便项目被信任,项目层也进不了凭证和模型路由。PROJECT_LOCAL_CONFIG_DENYLIST(config/src/loader/mod.rs)直接删掉 openai_base_url、chatgpt_base_url、model_provider(s)、notify、profile(s)、otel……注释写明:项目配置来自仓库内容,不能决定凭证打到哪、本地跑哪些命令。信任打开的是 hooks / execpolicy / 项目配置里 允许 的那些键,不是整份 toml 的超管权。
otel 进 denylist 是同一理由:项目不能决定遥测打到哪。analytics/ / otel/ 把 B1 的 Event 收成 span 和计费事实,不当独立产品层(F4)。抄 harness 时值得抄的是「Event 先按职分流,再投影成遥测」,不是抄一份导出客户端,更不是让仓库 .codex/config.toml 指定 collector。
Profile 是用户层的切片,不是项目层的。选了 profile,就多叠一份 ${CODEX_HOME}/<name>.config.toml。项目 toml 里写 profile = 会被 denylist 丢掉,不能让仓库替你换配置人格。
没有 origins 和 denylist 会怎样?你不知道 approval_policy 是管理员定的还是自己定的;恶意仓库用 .codex/config.toml 把 API 指到钓鱼站。分层的价值是 可追责的覆盖,不是「能 merge 就行」。
3. 未信任项目:文件会读,层不会生效
TrustLevel 只有 trusted / untrusted(protocol/src/config_types.rs:636-644)。信任记在用户(或托管)配置的 [projects] 里,按项目根,不是「我信这个模型」。项目根怎么算不是配置层自己猜的:config/src/loader/mod.rs 直接调 codex_git_utils::resolve_root_git_project_for_trust 找 Git 仓库根,再拿它算信任键(normalized_project_trust_keys)。子目录、monorepo 子包,信任记的是仓库根,不是你 cd 进去的那层路径。
未信任时,项目层 仍然被 load,但带 disabled_reason,不参与 effective_config()(loader README :48-50)。cwd / 祖先 / repo 的 .codex/config.toml 都是「loaded but disabled when untrusted」(loader/mod.rs:123-125)。禁用文案写得很具体:要加载的是 project-local config、hooks、and exec policies(:1078-1088)。TUI 启动会走目录信任 onboarding(tui/src/onboarding/directory_trust.rs:1-3)。
这和 B3 的 AskForApproval::UnlessTrusted、B4 的「未信任不读项目 AGENTS.md」是同一道闸的三面:执行要问、说明书不进、项目配置不生效。闸门在用户配置里,不在仓库里——否则恶意项目写 trust_level = trusted 就能自己给自己开门。
没有这道闸会怎样?git clone 等于执行项目 hook、改 execpolicy、把 MCP 写进你的会话。项目层默认不可信,不是 UX 吝啬,是把「仓库里的文件」和「能改你机器行为的配置」切开。
4. 鉴权:怎么登都进同一只 AuthManager
AuthMode 的能力分叉见 A1(uses_codex_backend())。B5 问两件更底层的事:凭证存在哪,以及 几条登录路是不是同一套运行时。
AuthDotJson 预期结构是 $CODEX_HOME/auth.json(login/src/auth/storage.rs:39-49):auth_mode、API key、ChatGPT tokens。存放模式 AuthCredentialsStoreMode(config/src/types.rs:111-120):
File— 默认,明文落在auth.json(权限应收紧;Unix 写文件会设 mode)Keyring— 系统钥匙串,拿不到就失败Auto— 有钥匙串用钥匙串,否则回退文件Ephemeral— 只在本进程内存,不落盘
企业可以 ForcedLoginMethod::{Chatgpt, Api}(protocol/src/config_types.rs:558-561),对应 A1 提过的「只许 ChatGPT / 只许 API Key」拒绝文案。再往上,MDM 和 EnterpriseManaged 层能在用户 toml 之前钉死审批、沙箱、允许的登录方式。那是托管约束,不是第三种模型供应商。
登进去的路径可以有好几条,落点只有一个 CodexAuth(login/src/auth/manager.rs:80-89):
| 路 | 怎么进 | 还是不是 AuthManager |
|---|---|---|
| ChatGPT 浏览器 | 本机短命 OAuth 回调(login/src/server.rs:1-3)+ PKCE(pkce.rs:12-27) | 是 |
| ChatGPT 设备码 | request_device_code / run_device_code_login(device_code_auth.rs) | 是 |
| API Key | login_with_api_key(manager.rs:995) | 是 |
| PAT / Agent Identity / Bedrock / Headers | 同一枚举上的变体 | 是 |
| Workload identity | AuthManager 初始化时可选绑上(manager.rs:2049-2067) | 是 |
交互登录的回调服务器故意把「给人看的错误」和「结构化日志」拆开,避免把 query 里的秘密写进普通日志(server.rs:5-13)。那是登录 UX,不是第二套身份根。
刷新和吊销也走这只 manager:UnauthorizedRecovery 按 managed / external 分步 reload 或 refresh(:1856-1869);logout_with_revoke 先吊销再清所有 store(:967-991)。六个入口读同一 AuthManager,才有 A1 的「一套账号胶水」。不要为 TUI、exec、IDE 各存一份 cookie。
没有统一存储和统一刷新会怎样?TUI 登了 ChatGPT,codex exec 却去环境变量里找 key;IDE 的 app-server 读到另一份过期 token,401 之后各写各的重登。鉴权是身份根上的状态,不是每个表面自己的 cookie。
5. 两套持久化:JSONL 是会话真相,SQLite 是索引
B4 的 rollout 是 可重放的事件流,目录在 $CODEX_HOME/sessions/(以及 archived_sessions/,rollout/src/lib.rs:84-85)。
StateDbHandle 是 SQLite 运行时(rollout/src/state_db.rs:29-44)。进程入口 init:打开库、按需把 rollout 元数据 backfill 进去,给 thread 列表、搜索、IDE 的 thread/list 用。它索引会话,不替代 JSONL。rollout 坏了,sqlite 里的标题救不回模型上下文;sqlite 暂时起不来,init 失败会警告并返回 None,不该把整个 CLI 打死——会话文件还在。
Session 里 settings 落盘用独立 semaphore(A2 提过),避免存储 I/O 堵住 turn。这是同一哲学:运行时状态和磁盘索引寿命不同,锁也要分开。
分页 fork、revert、跨进程队列、把旧 JSONL 迁进可分页 store,是下一层的事,见 G2。B5 只钉这句:能 resume 模型上下文的是事件流;能在 IDE 里翻出来的是索引。
没有「事件流 + 索引」会怎样?要么每次打开会话列表都扫全部 JSONL;要么只信 sqlite、compact 边界从索引里消失。B4 重建 Session 走 JSONL;B5 的 StateDb 让产品表面能列出、归档、搜索那些文件。
6. Feature flags、codex doctor、/status:有效值的三面镜子
功能开关集中在 codex-features(features/src/lib.rs:1-4)。Feature 枚举带阶段:UnderDevelopment / Experimental / Stable / Deprecated / Removed(:44-60)。CLI codex features list|enable|disable 写的是用户 config.toml(cli/src/main.rs:240-241, 1101-1107),仍要过分层合并——管理员 requirements 可以不许你打开某项。B2 的 UnifiedExec、B4 的 TokenBudget,都是这里的键,不是散落的 if cfg。注册表、合并链、/experimental 发现面见 G4。
codex doctor 是 只读体检(cli/src/doctor.rs:1-12):看安装、配置、鉴权、终端、磁盘、有限连通性,不修复、不拉长服务。PATH 上的程序名当不可信数据,可以看,不许拿来执行。给人看的摘要和 --json 同源。坏了该描述问题和怎么修,不该改用户状态。
TUI /status 的说明是「show current session configuration and token usage」(tui/src/slash_command.rs:52, 112)。它是 当前 Session 有效配置的投影:模型、审批、沙箱、额度。不是第四份 toml。要查「为什么是这个值」,去 origins,不要改 status 卡片。
这三面镜子分别回答:能开哪些实验、机器是否健康、这一轮实际用了哪套旋钮。它们都不拥有真相——真相仍是 layer stack + CODEX_HOME。
7. 结语:带走一句话
配置可以按项目叠,但账号、历史和「能不能执行仓库里的配置」必须住在 CODEX_HOME 的信任边界上——未信任项目读得到 .codex/,折进有效配置的只能是你(或管理员)明确允许的那几层。