Add session config loader interface (#18208)

## Why

Cloud-hosted sessions need a way for the service that starts or manages
a thread to provide session-owned config without treating all config as
if it came from the same user/project/workspace TOML stack.

The important boundary is ownership: some values should be controlled by
the session/orchestrator, some by the authenticated user, and later some
may come from the executor. The earlier broad config-store shape made
that boundary too fuzzy and overlapped heavily with the existing
filesystem-backed config loader. This PR starts with the smaller piece
we need now: a typed session config loader that can feed the existing
config layer stack while preserving the normal precedence and merge
behavior.

## What Changed

- Added `ThreadConfigLoader` and related typed payloads in
`codex-config`.
- `SessionThreadConfig` currently supports `model_provider`,
`model_providers`, and feature flags.
- `UserThreadConfig` is present as an ownership boundary, but does not
yet add TOML-backed fields.
- `NoopThreadConfigLoader` preserves existing behavior when no external
loader is configured.
  - `StaticThreadConfigLoader` supports tests and simple callers.

- Taught thread config sources to produce ordinary `ConfigLayerEntry`
values so the existing `ConfigLayerStack` remains the place where
precedence and merging happen.

- Wired the loader through `ConfigBuilder`, the config loader, and
app-server startup paths so app-server can provide session-owned config
before deriving a thread config.

- Added coverage for:
  - translating typed thread config into config layers,
- inserting thread config layers into the stack at the right precedence,
- applying session-provided model provider and feature settings when
app-server derives config from thread params.

## Follow-Ups

This intentionally stops short of adding the remote/service transport.
The next pieces are expected to be:

1. Define the proto/API shape for this interface.
2. Add a client implementation that can source session config from the
service side.

## Verification

- Added unit coverage in `codex-config` for the loader and layer
conversion.
- Added `codex-core` config loader coverage for thread config layer
precedence.
- Added app-server coverage that verifies session thread config wins
over request-provided config for model provider and feature settings.
This commit is contained in:
Rasmus Rygaard
2026-04-20 23:05:49 +00:00
committed by GitHub
parent 513dc28717
commit 7b994100b3
21 changed files with 553 additions and 2 deletions
+2
View File
@@ -2110,6 +2110,7 @@ async fn managed_config_overrides_oauth_store_mode() -> anyhow::Result<()> {
&Vec::new(),
overrides,
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
)
.await?;
let cfg =
@@ -2244,6 +2245,7 @@ async fn managed_config_wins_over_cli_overrides() -> anyhow::Result<()> {
&[("model".to_string(), TomlValue::String("cli".to_string()))],
overrides,
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
)
.await?;
+18 -1
View File
@@ -21,6 +21,7 @@ use crate::unified_exec::MIN_EMPTY_YIELD_TIME_MS;
use crate::windows_sandbox::WindowsSandboxLevelExt;
use crate::windows_sandbox::resolve_windows_sandbox_mode;
use crate::windows_sandbox::resolve_windows_sandbox_private_desktop;
use codex_config::ThreadConfigLoader;
use codex_config::config_toml::ConfigToml;
use codex_config::config_toml::ProjectConfig;
use codex_config::config_toml::RealtimeAudioConfig;
@@ -90,6 +91,7 @@ use std::collections::HashMap;
use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use crate::config::permissions::compile_permission_profile;
use crate::config::permissions::get_readable_roots_required_for_codex_runtime;
@@ -646,13 +648,14 @@ impl AuthManagerConfig for Config {
}
}
#[derive(Debug, Clone, Default)]
#[derive(Clone, Default)]
pub struct ConfigBuilder {
codex_home: Option<PathBuf>,
cli_overrides: Option<Vec<(String, TomlValue)>>,
harness_overrides: Option<ConfigOverrides>,
loader_overrides: Option<LoaderOverrides>,
cloud_requirements: CloudRequirementsLoader,
thread_config_loader: Option<Arc<dyn ThreadConfigLoader>>,
fallback_cwd: Option<PathBuf>,
}
@@ -682,6 +685,14 @@ impl ConfigBuilder {
self
}
pub fn thread_config_loader(
mut self,
thread_config_loader: Arc<dyn ThreadConfigLoader>,
) -> Self {
self.thread_config_loader = Some(thread_config_loader);
self
}
pub fn fallback_cwd(mut self, fallback_cwd: Option<PathBuf>) -> Self {
self.fallback_cwd = fallback_cwd;
self
@@ -694,6 +705,7 @@ impl ConfigBuilder {
harness_overrides,
loader_overrides,
cloud_requirements,
thread_config_loader,
fallback_cwd,
} = self;
let codex_home = match codex_home {
@@ -716,6 +728,9 @@ impl ConfigBuilder {
&cli_overrides,
loader_overrides,
cloud_requirements,
thread_config_loader
.as_deref()
.unwrap_or(&codex_config::NoopThreadConfigLoader),
)
.await?;
let merged_toml = config_layer_stack.effective_config();
@@ -894,6 +909,7 @@ pub async fn load_config_as_toml_with_cli_and_loader_overrides(
&cli_overrides,
loader_overrides,
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
)
.await?;
@@ -1065,6 +1081,7 @@ pub async fn load_global_mcp_servers(
&cli_overrides,
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
)
.await?;
let merged_toml = config_layer_stack.effective_config();
+1
View File
@@ -431,6 +431,7 @@ impl ConfigService {
&self.cli_overrides,
self.loader_overrides.clone(),
self.cloud_requirements.clone(),
&codex_config::NoopThreadConfigLoader,
)
.await
}