Skip to content

Commit a5b7675

Browse files
authored
add(core): managed config (#3868)
## Summary - Factor `load_config_as_toml` into `core::config_loader` so config loading is reusable across callers. - Layer `~/.codex/config.toml`, optional `~/.codex/managed_config.toml`, and macOS managed preferences (base64) with recursive table merging and scoped threads per source. ## Config Flow ``` Managed prefs (macOS profile: com.openai.codex/config_toml_base64) ▲ │ ~/.codex/managed_config.toml │ (optional file-based override) ▲ │ ~/.codex/config.toml (user-defined settings) ``` - The loader searches under the resolved `CODEX_HOME` directory (defaults to `~/.codex`). - Managed configs let administrators ship fleet-wide overrides via device profiles which is useful for enforcing certain settings like sandbox or approval defaults. - For nested hash tables: overlays merge recursively. Child tables are merged key-by-key, while scalar or array values replace the prior layer entirely. This lets admins add or tweak individual fields without clobbering unrelated user settings.
1 parent 9823de3 commit a5b7675

File tree

21 files changed

+675
-193
lines changed

21 files changed

+675
-193
lines changed

codex-rs/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/app-server/src/codex_message_processor.rs

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ impl CodexMessageProcessor {
500500
}
501501

502502
async fn get_user_saved_config(&self, request_id: RequestId) {
503-
let toml_value = match load_config_as_toml(&self.config.codex_home) {
503+
let toml_value = match load_config_as_toml(&self.config.codex_home).await {
504504
Ok(val) => val,
505505
Err(err) => {
506506
let error = JSONRPCErrorError {
@@ -653,18 +653,19 @@ impl CodexMessageProcessor {
653653
}
654654

655655
async fn process_new_conversation(&self, request_id: RequestId, params: NewConversationParams) {
656-
let config = match derive_config_from_params(params, self.codex_linux_sandbox_exe.clone()) {
657-
Ok(config) => config,
658-
Err(err) => {
659-
let error = JSONRPCErrorError {
660-
code: INVALID_REQUEST_ERROR_CODE,
661-
message: format!("error deriving config: {err}"),
662-
data: None,
663-
};
664-
self.outgoing.send_error(request_id, error).await;
665-
return;
666-
}
667-
};
656+
let config =
657+
match derive_config_from_params(params, self.codex_linux_sandbox_exe.clone()).await {
658+
Ok(config) => config,
659+
Err(err) => {
660+
let error = JSONRPCErrorError {
661+
code: INVALID_REQUEST_ERROR_CODE,
662+
message: format!("error deriving config: {err}"),
663+
data: None,
664+
};
665+
self.outgoing.send_error(request_id, error).await;
666+
return;
667+
}
668+
};
668669

669670
match self.conversation_manager.new_conversation(config).await {
670671
Ok(conversation_id) => {
@@ -752,7 +753,7 @@ impl CodexMessageProcessor {
752753
// Derive a Config using the same logic as new conversation, honoring overrides if provided.
753754
let config = match params.overrides {
754755
Some(overrides) => {
755-
derive_config_from_params(overrides, self.codex_linux_sandbox_exe.clone())
756+
derive_config_from_params(overrides, self.codex_linux_sandbox_exe.clone()).await
756757
}
757758
None => Ok(self.config.as_ref().clone()),
758759
};
@@ -1320,7 +1321,7 @@ async fn apply_bespoke_event_handling(
13201321
}
13211322
}
13221323

1323-
fn derive_config_from_params(
1324+
async fn derive_config_from_params(
13241325
params: NewConversationParams,
13251326
codex_linux_sandbox_exe: Option<PathBuf>,
13261327
) -> std::io::Result<Config> {
@@ -1358,7 +1359,7 @@ fn derive_config_from_params(
13581359
.map(|(k, v)| (k, json_to_toml(v)))
13591360
.collect();
13601361

1361-
Config::load_with_cli_overrides(cli_overrides, overrides)
1362+
Config::load_with_cli_overrides(cli_overrides, overrides).await
13621363
}
13631364

13641365
async fn on_patch_approval_response(

codex-rs/app-server/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ pub async fn run_main(
8181
)
8282
})?;
8383
let config = Config::load_with_cli_overrides(cli_kv_overrides, ConfigOverrides::default())
84+
.await
8485
.map_err(|e| {
8586
std::io::Error::new(ErrorKind::InvalidData, format!("error loading config: {e}"))
8687
})?;

codex-rs/chatgpt/src/apply_command.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ pub async fn run_apply_command(
2929
.parse_overrides()
3030
.map_err(anyhow::Error::msg)?,
3131
ConfigOverrides::default(),
32-
)?;
32+
)
33+
.await?;
3334

3435
init_chatgpt_token_from_auth(&config.codex_home).await?;
3536

codex-rs/cli/src/debug_sandbox.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ async fn run_command_under_sandbox(
7373
codex_linux_sandbox_exe,
7474
..Default::default()
7575
},
76-
)?;
76+
)
77+
.await?;
7778

7879
// In practice, this should be `std::env::current_dir()` because this CLI
7980
// does not support `--cwd`, but let's use the config value for consistency.

codex-rs/cli/src/login.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub async fn login_with_chatgpt(codex_home: PathBuf) -> std::io::Result<()> {
2626
}
2727

2828
pub async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) -> ! {
29-
let config = load_config_or_exit(cli_config_overrides);
29+
let config = load_config_or_exit(cli_config_overrides).await;
3030

3131
match login_with_chatgpt(config.codex_home).await {
3232
Ok(_) => {
@@ -44,7 +44,7 @@ pub async fn run_login_with_api_key(
4444
cli_config_overrides: CliConfigOverrides,
4545
api_key: String,
4646
) -> ! {
47-
let config = load_config_or_exit(cli_config_overrides);
47+
let config = load_config_or_exit(cli_config_overrides).await;
4848

4949
match login_with_api_key(&config.codex_home, &api_key) {
5050
Ok(_) => {
@@ -91,7 +91,7 @@ pub async fn run_login_with_device_code(
9191
issuer_base_url: Option<String>,
9292
client_id: Option<String>,
9393
) -> ! {
94-
let config = load_config_or_exit(cli_config_overrides);
94+
let config = load_config_or_exit(cli_config_overrides).await;
9595
let mut opts = ServerOptions::new(
9696
config.codex_home,
9797
client_id.unwrap_or(CLIENT_ID.to_string()),
@@ -112,7 +112,7 @@ pub async fn run_login_with_device_code(
112112
}
113113

114114
pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
115-
let config = load_config_or_exit(cli_config_overrides);
115+
let config = load_config_or_exit(cli_config_overrides).await;
116116

117117
match CodexAuth::from_codex_home(&config.codex_home) {
118118
Ok(Some(auth)) => match auth.mode {
@@ -143,7 +143,7 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
143143
}
144144

145145
pub async fn run_logout(cli_config_overrides: CliConfigOverrides) -> ! {
146-
let config = load_config_or_exit(cli_config_overrides);
146+
let config = load_config_or_exit(cli_config_overrides).await;
147147

148148
match logout(&config.codex_home) {
149149
Ok(true) => {
@@ -161,7 +161,7 @@ pub async fn run_logout(cli_config_overrides: CliConfigOverrides) -> ! {
161161
}
162162
}
163163

164-
fn load_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config {
164+
async fn load_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config {
165165
let cli_overrides = match cli_config_overrides.parse_overrides() {
166166
Ok(v) => v,
167167
Err(e) => {
@@ -171,7 +171,7 @@ fn load_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config {
171171
};
172172

173173
let config_overrides = ConfigOverrides::default();
174-
match Config::load_with_cli_overrides(cli_overrides, config_overrides) {
174+
match Config::load_with_cli_overrides(cli_overrides, config_overrides).await {
175175
Ok(config) => config,
176176
Err(e) => {
177177
eprintln!("Error loading configuration: {e}");

codex-rs/cli/src/mcp_cmd.rs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -113,30 +113,30 @@ impl McpCli {
113113

114114
match subcommand {
115115
McpSubcommand::List(args) => {
116-
run_list(&config_overrides, args)?;
116+
run_list(&config_overrides, args).await?;
117117
}
118118
McpSubcommand::Get(args) => {
119-
run_get(&config_overrides, args)?;
119+
run_get(&config_overrides, args).await?;
120120
}
121121
McpSubcommand::Add(args) => {
122-
run_add(&config_overrides, args)?;
122+
run_add(&config_overrides, args).await?;
123123
}
124124
McpSubcommand::Remove(args) => {
125-
run_remove(&config_overrides, args)?;
125+
run_remove(&config_overrides, args).await?;
126126
}
127127
McpSubcommand::Login(args) => {
128128
run_login(&config_overrides, args).await?;
129129
}
130130
McpSubcommand::Logout(args) => {
131-
run_logout(&config_overrides, args)?;
131+
run_logout(&config_overrides, args).await?;
132132
}
133133
}
134134

135135
Ok(())
136136
}
137137
}
138138

139-
fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Result<()> {
139+
async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Result<()> {
140140
// Validate any provided overrides even though they are not currently applied.
141141
config_overrides.parse_overrides().map_err(|e| anyhow!(e))?;
142142

@@ -162,6 +162,7 @@ fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Result<(
162162

163163
let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;
164164
let mut servers = load_global_mcp_servers(&codex_home)
165+
.await
165166
.with_context(|| format!("failed to load MCP servers from {}", codex_home.display()))?;
166167

167168
let new_entry = McpServerConfig {
@@ -184,7 +185,7 @@ fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Result<(
184185
Ok(())
185186
}
186187

187-
fn run_remove(config_overrides: &CliConfigOverrides, remove_args: RemoveArgs) -> Result<()> {
188+
async fn run_remove(config_overrides: &CliConfigOverrides, remove_args: RemoveArgs) -> Result<()> {
188189
config_overrides.parse_overrides().map_err(|e| anyhow!(e))?;
189190

190191
let RemoveArgs { name } = remove_args;
@@ -193,6 +194,7 @@ fn run_remove(config_overrides: &CliConfigOverrides, remove_args: RemoveArgs) ->
193194

194195
let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;
195196
let mut servers = load_global_mcp_servers(&codex_home)
197+
.await
196198
.with_context(|| format!("failed to load MCP servers from {}", codex_home.display()))?;
197199

198200
let removed = servers.remove(&name).is_some();
@@ -214,6 +216,7 @@ fn run_remove(config_overrides: &CliConfigOverrides, remove_args: RemoveArgs) ->
214216
async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs) -> Result<()> {
215217
let overrides = config_overrides.parse_overrides().map_err(|e| anyhow!(e))?;
216218
let config = Config::load_with_cli_overrides(overrides, ConfigOverrides::default())
219+
.await
217220
.context("failed to load configuration")?;
218221

219222
if !config.use_experimental_use_rmcp_client {
@@ -238,9 +241,10 @@ async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs)
238241
Ok(())
239242
}
240243

241-
fn run_logout(config_overrides: &CliConfigOverrides, logout_args: LogoutArgs) -> Result<()> {
244+
async fn run_logout(config_overrides: &CliConfigOverrides, logout_args: LogoutArgs) -> Result<()> {
242245
let overrides = config_overrides.parse_overrides().map_err(|e| anyhow!(e))?;
243246
let config = Config::load_with_cli_overrides(overrides, ConfigOverrides::default())
247+
.await
244248
.context("failed to load configuration")?;
245249

246250
let LogoutArgs { name } = logout_args;
@@ -264,9 +268,10 @@ fn run_logout(config_overrides: &CliConfigOverrides, logout_args: LogoutArgs) ->
264268
Ok(())
265269
}
266270

267-
fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) -> Result<()> {
271+
async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) -> Result<()> {
268272
let overrides = config_overrides.parse_overrides().map_err(|e| anyhow!(e))?;
269273
let config = Config::load_with_cli_overrides(overrides, ConfigOverrides::default())
274+
.await
270275
.context("failed to load configuration")?;
271276

272277
let mut entries: Vec<_> = config.mcp_servers.iter().collect();
@@ -424,9 +429,10 @@ fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) -> Resul
424429
Ok(())
425430
}
426431

427-
fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Result<()> {
432+
async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Result<()> {
428433
let overrides = config_overrides.parse_overrides().map_err(|e| anyhow!(e))?;
429434
let config = Config::load_with_cli_overrides(overrides, ConfigOverrides::default())
435+
.await
430436
.context("failed to load configuration")?;
431437

432438
let Some(server) = config.mcp_servers.get(&get_args.name) else {

codex-rs/cli/tests/mcp_add_remove.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ fn codex_command(codex_home: &Path) -> Result<assert_cmd::Command> {
1313
Ok(cmd)
1414
}
1515

16-
#[test]
17-
fn add_and_remove_server_updates_global_config() -> Result<()> {
16+
#[tokio::test]
17+
async fn add_and_remove_server_updates_global_config() -> Result<()> {
1818
let codex_home = TempDir::new()?;
1919

2020
let mut add_cmd = codex_command(codex_home.path())?;
@@ -24,7 +24,7 @@ fn add_and_remove_server_updates_global_config() -> Result<()> {
2424
.success()
2525
.stdout(contains("Added global MCP server 'docs'."));
2626

27-
let servers = load_global_mcp_servers(codex_home.path())?;
27+
let servers = load_global_mcp_servers(codex_home.path()).await?;
2828
assert_eq!(servers.len(), 1);
2929
let docs = servers.get("docs").expect("server should exist");
3030
match &docs.transport {
@@ -43,7 +43,7 @@ fn add_and_remove_server_updates_global_config() -> Result<()> {
4343
.success()
4444
.stdout(contains("Removed global MCP server 'docs'."));
4545

46-
let servers = load_global_mcp_servers(codex_home.path())?;
46+
let servers = load_global_mcp_servers(codex_home.path()).await?;
4747
assert!(servers.is_empty());
4848

4949
let mut remove_again_cmd = codex_command(codex_home.path())?;
@@ -53,14 +53,14 @@ fn add_and_remove_server_updates_global_config() -> Result<()> {
5353
.success()
5454
.stdout(contains("No MCP server named 'docs' found."));
5555

56-
let servers = load_global_mcp_servers(codex_home.path())?;
56+
let servers = load_global_mcp_servers(codex_home.path()).await?;
5757
assert!(servers.is_empty());
5858

5959
Ok(())
6060
}
6161

62-
#[test]
63-
fn add_with_env_preserves_key_order_and_values() -> Result<()> {
62+
#[tokio::test]
63+
async fn add_with_env_preserves_key_order_and_values() -> Result<()> {
6464
let codex_home = TempDir::new()?;
6565

6666
let mut add_cmd = codex_command(codex_home.path())?;
@@ -80,7 +80,7 @@ fn add_with_env_preserves_key_order_and_values() -> Result<()> {
8080
.assert()
8181
.success();
8282

83-
let servers = load_global_mcp_servers(codex_home.path())?;
83+
let servers = load_global_mcp_servers(codex_home.path()).await?;
8484
let envy = servers.get("envy").expect("server should exist");
8585
let env = match &envy.transport {
8686
McpServerTransportConfig::Stdio { env: Some(env), .. } => env,

codex-rs/core/Cargo.toml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ async-trait = { workspace = true }
1919
base64 = { workspace = true }
2020
bytes = { workspace = true }
2121
chrono = { workspace = true, features = ["serde"] }
22+
codex-app-server-protocol = { workspace = true }
2223
codex-apply-patch = { workspace = true }
2324
codex-file-search = { workspace = true }
2425
codex-mcp-client = { workspace = true }
25-
codex-rmcp-client = { workspace = true }
26-
codex-protocol = { workspace = true }
27-
codex-app-server-protocol = { workspace = true }
2826
codex-otel = { workspace = true, features = ["otel"] }
27+
codex-protocol = { workspace = true }
28+
codex-rmcp-client = { workspace = true }
2929
codex-utils-string = { workspace = true }
3030
dirs = { workspace = true }
3131
dunce = { workspace = true }
@@ -76,6 +76,9 @@ wildmatch = { workspace = true }
7676
landlock = { workspace = true }
7777
seccompiler = { workspace = true }
7878

79+
[target.'cfg(target_os = "macos")'.dependencies]
80+
core-foundation = "0.9"
81+
7982
# Build OpenSSL from source for musl builds.
8083
[target.x86_64-unknown-linux-musl.dependencies]
8184
openssl-sys = { workspace = true, features = ["vendored"] }

0 commit comments

Comments
 (0)