Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions codex-rs/core/src/config/agent_roles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ pub(crate) async fn load_agent_roles(
for layer in layers {
let mut layer_roles: BTreeMap<String, AgentRoleConfig> = BTreeMap::new();
let mut declared_role_files = BTreeSet::new();
let agents_toml = match agents_toml_from_layer(&layer.config) {
let config_folder = layer.config_folder();
let agents_toml = match agents_toml_from_layer(&layer.config, config_folder.as_deref()) {
Ok(agents_toml) => agents_toml,
Err(err) => {
push_agent_role_warning(startup_warnings, err);
Expand Down Expand Up @@ -169,11 +170,16 @@ fn merge_missing_role_fields(role: &mut AgentRoleConfig, fallback: &AgentRoleCon
.or(fallback.nickname_candidates.clone());
}

fn agents_toml_from_layer(layer_toml: &TomlValue) -> std::io::Result<Option<AgentsToml>> {
fn agents_toml_from_layer(
layer_toml: &TomlValue,
config_base_dir: Option<&Path>,
) -> std::io::Result<Option<AgentsToml>> {
let Some(agents_toml) = layer_toml.get("agents") else {
return Ok(None);
};

// AbsolutePathBufGuard resolves relative paths while it remains in scope.
let _guard = config_base_dir.map(AbsolutePathBufGuard::new);
agents_toml
.clone()
.try_into()
Expand Down
57 changes: 57 additions & 0 deletions codex-rs/core/src/config/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4045,6 +4045,63 @@ nickname_candidates = ["Hypatia", "Noether"]
Ok(())
}

#[tokio::test]
async fn agent_role_relative_config_file_resolves_from_config_layer() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let role_config_path = codex_home.path().join("agents").join("researcher.toml");
tokio::fs::create_dir_all(
role_config_path
.parent()
.expect("role config should have a parent directory"),
)
.await?;
tokio::fs::write(
&role_config_path,
"developer_instructions = \"Research carefully\"\nmodel = \"gpt-5\"",
)
.await?;
let layer_config = toml::from_str(
r#"[agents.researcher]
description = "Research role"
config_file = "./agents/researcher.toml"
"#,
)
.expect("agent role layer config should parse");
let config_layer_stack = crate::config_loader::ConfigLayerStack::new(
vec![crate::config_loader::ConfigLayerEntry::new(
codex_app_server_protocol::ConfigLayerSource::User {
file: codex_home.path().join(CONFIG_TOML_FILE).abs(),
},
layer_config,
)],
Default::default(),
crate::config_loader::ConfigRequirementsToml::default(),
)
.map_err(std::io::Error::other)?;

let config = Config::load_config_with_layer_stack(
LOCAL_FS.as_ref(),
ConfigToml::default(),
ConfigOverrides {
cwd: Some(codex_home.path().to_path_buf()),
..Default::default()
},
codex_home.abs(),
config_layer_stack,
)
.await?;

assert_eq!(
config
.agent_roles
.get("researcher")
.and_then(|role| role.config_file.as_ref()),
Some(&role_config_path)
);

Ok(())
}

#[tokio::test]
async fn agent_role_file_metadata_overrides_config_toml_metadata() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
Expand Down
Loading