feat: export and replay effective config locks (#20405)

## Why

For reproducibility. A hand-written `config.toml` is not enough to
recreate what a Codex session actually ran with because layered config,
CLI overrides, defaults, feature aliases, resolved feature config,
prompt setup, and model-catalog/session values can all affect the final
runtime behavior.

This PR adds an effective config lockfile path: one run can export the
resolved session config, and a later run can replay that lockfile and
fail early if the regenerated effective config drifts.

## What Changed

- Add a dedicated `ConfigLockfileToml` wrapper with top-level lockfile
metadata plus the replayable config:

  ```toml
  version = 1
  codex_version = "..."

  [config]
  # effective ConfigToml fields
  ```

- Keep lockfile metadata out of regular `ConfigToml`; replay loads
`ConfigLockfileToml` and then uses its nested `config` as the
authoritative config layer.
- Add `debug.config_lockfile.export_dir` to write
`<thread_id>.config.lock.toml` when a root session starts.
- Add `debug.config_lockfile.load_path` to replay a saved lockfile and
validate the regenerated session lockfile against it.
- Add `debug.config_lockfile.allow_codex_version_mismatch` to optionally
tolerate Codex binary version drift while still comparing the rest of
the lockfile.
- Add `debug.config_lockfile.save_fields_resolved_from_model_catalog` so
lock creation can either save model-catalog/session-resolved fields or
intentionally leave those fields dynamic.
- Build lockfiles from the effective config plus resolved runtime values
such as model selection, reasoning settings, prompts, service tier, web
search mode, feature states/config, memories config, skill instructions,
and agent limits.
- Materialize feature aliases and custom feature config into the
lockfile so replay compares canonical resolved behavior instead of
user-authored alias shape.
- Strip profile/debug/file-include/environment-specific inputs from
generated lockfiles so they contain replayable values rather than the
inputs that produced those values.
- Surface JSON-RPC server error code/data in app-server client and TUI
bootstrap errors so config-lock replay failures include the actual TOML
diff.
- Regenerate the config schema for the new debug config keys.

## Review Notes

The main flow is split across these files:

- `config/src/config_toml.rs`: lockfile/debug TOML shapes.
- `core/src/config/mod.rs`: loading `debug.config_lockfile.*`, replaying
a lockfile as a config layer, and preserving the expected lockfile for
validation.
- `core/src/session/config_lock.rs`: exporting the current session
lockfile and materializing resolved session/config values.
- `core/src/config_lock.rs`: lockfile parsing, metadata/version checks,
replay comparison, and diff formatting.

## Usage

Export a lockfile from a normal session:

```sh
codex -c 'debug.config_lockfile.export_dir="/tmp/codex-locks"'
```

Export a lockfile without saving model-catalog/session-resolved fields:

```sh
codex -c 'debug.config_lockfile.export_dir="/tmp/codex-locks"' \
  -c 'debug.config_lockfile.save_fields_resolved_from_model_catalog=false'
```

Replay a saved lockfile in a later session:

```sh
codex -c 'debug.config_lockfile.load_path="/tmp/codex-locks/<thread_id>.config.lock.toml"'
```

If replay resolves to a different effective config, startup fails with a
TOML diff.

To tolerate Codex binary version drift during replay:

```sh
codex -c 'debug.config_lockfile.load_path="/tmp/codex-locks/<thread_id>.config.lock.toml"' \
  -c 'debug.config_lockfile.allow_codex_version_mismatch=true'
```

## Limitations

This does not support custom rules/network policies.

## Verification

- `cargo test -p codex-core config_lock`
- `cargo test -p codex-config`
- `cargo test -p codex-thread-manager-sample`
This commit is contained in:
jif-oai authored and GitHub committed 2026-05-01 17:46:02 +02:00
1 parent ff27d01676
commit 0b04d1b3cc
17 files changed
+977 -16

No files matched your search

+62 -1
View File
@@ -65,6 +65,28 @@ const RESERVED_MODEL_PROVIDER_IDS: [&str; 4] = [
LMSTUDIO_OSS_PROVIDER_ID,
];
pub const DEFAULT_PROJECT_DOC_MAX_BYTES: usize = 32 * 1024;
const fn default_allow_login_shell() -> Option<bool> {
Some(true)
}
fn default_history() -> Option<History> {
Some(History::default())
}
const fn default_project_doc_max_bytes() -> Option<usize> {
Some(DEFAULT_PROJECT_DOC_MAX_BYTES)
}
fn default_project_doc_fallback_filenames() -> Option<Vec<String>> {
Some(Vec::new())
}
const fn default_hide_agent_reasoning() -> Option<bool> {
Some(false)
}
/// Base config deserialized from ~/.codex/config.toml.
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]
#[schemars(deny_unknown_fields)]
@@ -106,6 +128,7 @@ pub struct ConfigToml {
/// If `false`, the model can never use a login shell: `login = true`
/// requests are rejected, and omitting `login` defaults to a non-login
/// shell.
#[serde(default = "default_allow_login_shell")]
pub allow_login_shell: Option<bool>,
/// Sandbox mode to use.
@@ -202,9 +225,11 @@ pub struct ConfigToml {
pub model_providers: HashMap<String, ModelProviderInfo>,
/// Maximum number of bytes to include from an AGENTS.md project doc file.
#[serde(default = "default_project_doc_max_bytes")]
pub project_doc_max_bytes: Option<usize>,
/// Ordered list of fallback filenames to look for when AGENTS.md is missing.
#[serde(default = "default_project_doc_fallback_filenames")]
pub project_doc_fallback_filenames: Option<Vec<String>>,
/// Token budget applied when storing tool/function outputs in the context manager.
@@ -233,7 +258,7 @@ pub struct ConfigToml {
pub profiles: HashMap<String, ConfigProfile>,
/// Settings that govern if and what will be written to `~/.codex/history.jsonl`.
#[serde(default)]
#[serde(default = "default_history")]
pub history: Option<History>,
/// Directory where Codex stores the SQLite state DB.
@@ -244,6 +269,9 @@ pub struct ConfigToml {
/// Defaults to `$CODEX_HOME/log`.
pub log_dir: Option<AbsolutePathBuf>,
/// Debugging and reproducibility settings.
pub debug: Option<DebugToml>,
/// Optional URI-based file opener. If set, citations to files in the model
/// output will be hyperlinked using the specified URI scheme.
pub file_opener: Option<UriBasedFileOpener>,
@@ -253,6 +281,7 @@ pub struct ConfigToml {
/// When set to `true`, `AgentReasoning` events will be hidden from the
/// UI/output. Defaults to `false`.
#[serde(default = "default_hide_agent_reasoning")]
pub hide_agent_reasoning: Option<bool>,
/// When set to `true`, `AgentReasoningRawContentEvent` events will be shown in the UI/output.
@@ -420,6 +449,38 @@ pub struct ConfigToml {
pub oss_provider: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct ConfigLockfileToml {
pub version: u32,
pub codex_version: String,
/// Replayable effective config captured in the lockfile.
pub config: ConfigToml,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct DebugToml {
pub config_lockfile: Option<DebugConfigLockToml>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct DebugConfigLockToml {
/// Directory where Codex writes effective session config lock files.
pub export_dir: Option<AbsolutePathBuf>,
/// Lockfile to replay as the authoritative effective config.
pub load_path: Option<AbsolutePathBuf>,
/// Allow replaying a lock generated by a different Codex version.
pub allow_codex_version_mismatch: Option<bool>,
/// Save fields resolved from the model catalog/session configuration.
pub save_fields_resolved_from_model_catalog: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ThreadStoreToml {
+2 -1
View File
@@ -136,6 +136,7 @@ impl UriBasedFileOpener {
/// Settings that govern if and what will be written to `~/.codex/history.jsonl`.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
#[serde(default)]
#[schemars(deny_unknown_fields)]
pub struct History {
/// If true, history entries will not be written to disk.
@@ -262,7 +263,7 @@ pub struct MemoriesToml {
}
/// Effective memories settings after defaults are applied.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct MemoriesConfig {
pub disable_on_external_context: bool,
pub generate_memories: bool,