mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat: add layered --profile-v2 config files (#17141)
## Why `--profile-v2 <name>` gives launchers and runtime entry points a named profile config without making each profile duplicate the base user config. The base `$CODEX_HOME/config.toml` still loads first, then `$CODEX_HOME/<name>.config.toml` layers above it and becomes the active writable user config for that session. That keeps shared defaults, plugin/MCP setup, and managed/user constraints in one place while letting a named profile override only the pieces that need to differ. ## What Changed - Added the shared `--profile-v2 <name>` runtime option with validated plain names, now represented by `ProfileV2Name`. - Extended config layer state so the base user config and selected profile config are both `User` layers; APIs expose the active user layer and merged effective user config. - Threaded profile selection through runtime entry points: `codex`, `codex exec`, `codex review`, `codex resume`, `codex fork`, and `codex debug prompt-input`. - Made user-facing config writes go to the selected profile file when active, including TUI/settings persistence, app-server config writes, and MCP/app tool approval persistence. - Made plugin, marketplace, MCP, hooks, and config reload paths read from the merged user config so base and profile layers both participate. - Updated app-server config layer schemas to mark profile-backed user layers. ## Limits `--profile-v2` is still rejected for config-management subcommands such as feature, MCP, and marketplace edits. Those paths remain tied to the base `config.toml` until they have explicit profile-selection semantics. Some adjacent background writes may still update base or global state rather than the selected profile: - marketplace auto-upgrade metadata - automatic MCP dependency installs from skills - remote plugin sync or uninstall config edits - personality migration marker/default writes ## Verification Added targeted coverage for profile name validation, layer ordering/merging, selected-profile writes, app-server config writes, session hot reload, plugin config merging, hooks/config fixture updates, and MCP/app approval persistence. --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
17cd321c32
commit
deedf3b2c4
@@ -37,6 +37,7 @@ use codex_tui::ExitReason;
|
||||
use codex_tui::UpdateAction;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_cli::CliConfigOverrides;
|
||||
use codex_utils_cli::ProfileV2Name;
|
||||
use owo_colors::OwoColorize;
|
||||
use std::io::IsTerminal;
|
||||
use std::path::PathBuf;
|
||||
@@ -56,11 +57,13 @@ use crate::marketplace_cmd::MarketplaceCli;
|
||||
use crate::mcp_cmd::McpCli;
|
||||
use doctor::DoctorCommand;
|
||||
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_core::build_models_manager;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::config::edit::ConfigEditsBuilder;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_core::config::resolve_profile_v2_config_path;
|
||||
use codex_features::FEATURES;
|
||||
use codex_features::Stage;
|
||||
use codex_features::is_known_feature_key;
|
||||
@@ -853,6 +856,9 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
let root_remote_auth_token_env = remote.remote_auth_token_env;
|
||||
let root_strict_config = interactive.strict_config;
|
||||
reject_root_strict_config_for_subcommand(root_strict_config, &subcommand)?;
|
||||
if let Some(subcommand) = subcommand.as_ref() {
|
||||
profile_v2_for_subcommand(&interactive, subcommand)?;
|
||||
}
|
||||
|
||||
match subcommand {
|
||||
None => {
|
||||
@@ -895,6 +901,9 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
"review",
|
||||
)?;
|
||||
let mut exec_cli = ExecCli::try_parse_from(["codex", "exec"])?;
|
||||
exec_cli
|
||||
.shared
|
||||
.inherit_exec_root_options(&interactive.shared);
|
||||
exec_cli.command = Some(ExecCommand::Review(review_args));
|
||||
exec_cli.strict_config = strict_config || root_strict_config;
|
||||
prepend_config_flags(
|
||||
@@ -971,7 +980,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
codex_app_server::run_main_with_transport_options(
|
||||
arg0_paths.clone(),
|
||||
root_config_overrides,
|
||||
codex_config::LoaderOverrides::default(),
|
||||
LoaderOverrides::default(),
|
||||
strict_config,
|
||||
analytics_default_enabled,
|
||||
transport,
|
||||
@@ -1446,6 +1455,28 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn profile_v2_for_subcommand<'a>(
|
||||
interactive: &'a TuiCli,
|
||||
subcommand: &Subcommand,
|
||||
) -> anyhow::Result<Option<&'a ProfileV2Name>> {
|
||||
let Some(profile_v2) = interactive.config_profile_v2.as_ref() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
match subcommand {
|
||||
Subcommand::Exec(_)
|
||||
| Subcommand::Review(_)
|
||||
| Subcommand::Resume(_)
|
||||
| Subcommand::Fork(_)
|
||||
| Subcommand::Debug(DebugCommand {
|
||||
subcommand: DebugSubcommand::PromptInput(_),
|
||||
}) => Ok(Some(profile_v2)),
|
||||
_ => anyhow::bail!(
|
||||
"--profile-v2 only applies to runtime commands: `codex`, `codex exec`, `codex review`, `codex resume`, `codex fork`, and `codex debug prompt-input`."
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_exec_server_command(
|
||||
cmd: ExecServerCommand,
|
||||
arg0_paths: &Arg0DispatchPaths,
|
||||
@@ -1504,6 +1535,22 @@ async fn disable_feature_in_config(interactive: &TuiCli, feature: &str) -> anyho
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn loader_overrides_for_profile(
|
||||
profile_v2: Option<&ProfileV2Name>,
|
||||
) -> anyhow::Result<LoaderOverrides> {
|
||||
match profile_v2 {
|
||||
Some(profile_v2) => {
|
||||
let codex_home = find_codex_home()?;
|
||||
Ok(LoaderOverrides {
|
||||
user_config_path: Some(resolve_profile_v2_config_path(&codex_home, profile_v2)),
|
||||
user_config_profile: Some(profile_v2.clone()),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
None => Ok(LoaderOverrides::default()),
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_print_under_development_feature_warning(
|
||||
codex_home: &std::path::Path,
|
||||
interactive: &TuiCli,
|
||||
@@ -1546,6 +1593,7 @@ async fn run_debug_prompt_input_command(
|
||||
interactive: TuiCli,
|
||||
arg0_paths: Arg0DispatchPaths,
|
||||
) -> anyhow::Result<()> {
|
||||
let loader_overrides = loader_overrides_for_profile(interactive.config_profile_v2.as_ref())?;
|
||||
let shared = interactive.shared.into_inner();
|
||||
let mut cli_kv_overrides = root_config_overrides
|
||||
.parse_overrides()
|
||||
@@ -1585,6 +1633,7 @@ async fn run_debug_prompt_input_command(
|
||||
let config = ConfigBuilder::default()
|
||||
.cli_overrides(cli_kv_overrides)
|
||||
.harness_overrides(overrides)
|
||||
.loader_overrides(loader_overrides)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
@@ -2107,6 +2156,49 @@ mod tests {
|
||||
finalize_fork_interactive(interactive, root_overrides, session_id, last, all, fork_cli)
|
||||
}
|
||||
|
||||
fn profile_v2_for_args(args: &[&str]) -> anyhow::Result<Option<String>> {
|
||||
let cli = MultitoolCli::try_parse_from(args).expect("parse");
|
||||
let Some(subcommand) = cli.subcommand.as_ref() else {
|
||||
return Ok(cli
|
||||
.interactive
|
||||
.config_profile_v2
|
||||
.as_ref()
|
||||
.map(std::string::ToString::to_string));
|
||||
};
|
||||
Ok(profile_v2_for_subcommand(&cli.interactive, subcommand)?.map(ToString::to_string))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_v2_is_rejected_for_config_management_subcommands() {
|
||||
assert!(
|
||||
profile_v2_for_args(&["codex", "--profile-v2", "work", "features", "list"]).is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_v2_is_allowed_for_runtime_subcommands() {
|
||||
assert_eq!(
|
||||
profile_v2_for_args(&["codex", "--profile-v2", "work", "resume"])
|
||||
.expect("resume supports profile-v2")
|
||||
.as_deref(),
|
||||
Some("work")
|
||||
);
|
||||
assert_eq!(
|
||||
profile_v2_for_args(&["codex", "--profile-v2", "work", "debug", "prompt-input"])
|
||||
.expect("debug prompt-input supports profile-v2")
|
||||
.as_deref(),
|
||||
Some("work")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_v2_rejects_non_plain_names_at_parse_time() {
|
||||
assert!(
|
||||
MultitoolCli::try_parse_from(["codex", "--profile-v2", "nested/work", "resume"])
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_resume_last_accepts_prompt_positional() {
|
||||
let cli =
|
||||
@@ -2510,6 +2602,8 @@ mod tests {
|
||||
"gpt-5.1-test",
|
||||
"-p",
|
||||
"my-profile",
|
||||
"--profile-v2",
|
||||
"my-config",
|
||||
"-C",
|
||||
"/tmp",
|
||||
"--strict-config",
|
||||
@@ -2522,6 +2616,7 @@ mod tests {
|
||||
assert_eq!(interactive.model.as_deref(), Some("gpt-5.1-test"));
|
||||
assert!(interactive.oss);
|
||||
assert_eq!(interactive.config_profile.as_deref(), Some("my-profile"));
|
||||
assert_eq!(interactive.config_profile_v2.as_deref(), Some("my-config"));
|
||||
assert_matches!(
|
||||
interactive.sandbox_mode,
|
||||
Some(codex_utils_cli::SandboxModeCliArg::WorkspaceWrite)
|
||||
|
||||
Reference in New Issue
Block a user