Reject legacy profiles with profile v2

This commit is contained in:
jif-oai
2026-05-14 14:53:36 +01:00
parent deedf3b2c4
commit 146135a7dd
2 changed files with 162 additions and 12 deletions

View File

@@ -1,6 +1,8 @@
mod layer_io;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(test)]
mod tests;
use self::layer_io::LoadedConfigLayers;
use crate::CONFIG_TOML_FILE;
@@ -211,19 +213,33 @@ pub async fn load_config_layers_state(
// Add the base user config layer. When profile-v2 is selected, add the
// profile config as a second user layer on top so the profile only needs to
// contain overrides.
let base_user_file = AbsolutePathBuf::resolve_path_against_base(CONFIG_TOML_FILE, codex_home);
layers.push(
load_user_config_layer(
fs,
&base_user_file,
/*profile*/ None,
ignore_user_config,
strict_config,
)
.await?,
);
let active_user_file = overrides.user_config_path(codex_home)?;
let base_user_file = AbsolutePathBuf::resolve_path_against_base(CONFIG_TOML_FILE, codex_home);
let base_user_layer = load_user_config_layer(
fs,
&base_user_file,
/*profile*/ None,
ignore_user_config,
strict_config,
)
.await?;
if active_user_profile.is_some()
&& base_user_layer
.config
.as_table()
.is_some_and(|config| config.contains_key("profiles"))
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"--profile-v2 cannot be used while {} contains legacy `[profiles]` config; move those profile settings into a profile-v2 file such as {} or remove `[profiles]`",
base_user_file.as_path().display(),
active_user_file.as_path().display()
),
));
}
layers.push(base_user_layer);
if active_user_file != base_user_file {
layers.push(
load_user_config_layer(

View File

@@ -0,0 +1,134 @@
use super::*;
use async_trait::async_trait;
use codex_file_system::CopyOptions;
use codex_file_system::CreateDirectoryOptions;
use codex_file_system::FileMetadata;
use codex_file_system::FileSystemResult;
use codex_file_system::FileSystemSandboxContext;
use codex_file_system::ReadDirectoryEntry;
use codex_file_system::RemoveOptions;
use pretty_assertions::assert_eq;
use tempfile::tempdir;
struct TestFileSystem;
#[async_trait]
impl ExecutorFileSystem for TestFileSystem {
async fn read_file(
&self,
path: &AbsolutePathBuf,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<u8>> {
tokio::fs::read(path.as_path()).await
}
async fn write_file(
&self,
_path: &AbsolutePathBuf,
_contents: Vec<u8>,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
unimplemented!("test filesystem only supports reads")
}
async fn create_directory(
&self,
_path: &AbsolutePathBuf,
_create_directory_options: CreateDirectoryOptions,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
unimplemented!("test filesystem only supports reads")
}
async fn get_metadata(
&self,
_path: &AbsolutePathBuf,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<FileMetadata> {
unimplemented!("test filesystem only supports reads")
}
async fn read_directory(
&self,
_path: &AbsolutePathBuf,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
unimplemented!("test filesystem only supports reads")
}
async fn remove(
&self,
_path: &AbsolutePathBuf,
_remove_options: RemoveOptions,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
unimplemented!("test filesystem only supports reads")
}
async fn copy(
&self,
_source_path: &AbsolutePathBuf,
_destination_path: &AbsolutePathBuf,
_copy_options: CopyOptions,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
unimplemented!("test filesystem only supports reads")
}
}
#[tokio::test]
async fn profile_v2_rejects_legacy_profiles_in_base_user_config() {
let tmp = tempdir().expect("tempdir");
let selected_config = tmp.path().join("work.config.toml");
std::fs::write(
tmp.path().join(CONFIG_TOML_FILE),
r#"
model = "gpt-main"
[profiles.work]
model = "gpt-work"
"#,
)
.expect("write default user config");
std::fs::write(&selected_config, r#"model = "gpt-work-v2""#)
.expect("write selected user config");
let mut overrides = LoaderOverrides::without_managed_config_for_tests();
overrides.user_config_path = Some(AbsolutePathBuf::resolve_path_against_base(
"work.config.toml",
tmp.path(),
));
overrides.user_config_profile = Some("work".parse().expect("profile-v2 name"));
let err = load_config_layers_state(
&TestFileSystem,
tmp.path(),
/*cwd*/ None,
&[],
overrides,
CloudRequirementsLoader::default(),
&crate::NoopThreadConfigLoader,
)
.await
.expect_err("profile-v2 should reject legacy profiles in base user config");
assert_eq!(
err.kind(),
io::ErrorKind::InvalidData,
"legacy profiles should be a hard config error"
);
let message = err.to_string();
assert!(
message.contains("--profile-v2 cannot be used"),
"unexpected error message: {message}"
);
assert!(
message.contains("config.toml"),
"unexpected error message: {message}"
);
assert!(
message.contains("[profiles]"),
"unexpected error message: {message}"
);
}