Load agent metadata from role files (#14177)

This commit is contained in:
gabec-openai
2026-03-10 16:21:48 -07:00
committed by Michael Bolin
parent 3d41ff0b77
commit a67660da2d
5 changed files with 1293 additions and 135 deletions

View File

@@ -9,6 +9,7 @@
use crate::config::AgentRoleConfig;
use crate::config::Config;
use crate::config::ConfigOverrides;
use crate::config::agent_roles::parse_agent_role_file_contents;
use crate::config::deserialize_config_toml_with_base;
use crate::config_loader::ConfigLayerEntry;
use crate::config_loader::ConfigLayerStack;
@@ -46,26 +47,34 @@ pub(crate) async fn apply_role_to_config(
return Ok(());
};
let (role_config_contents, role_config_base) = if is_built_in {
(
built_in::config_file_contents(config_file)
.map(str::to_owned)
.ok_or_else(|| AGENT_TYPE_UNAVAILABLE_ERROR.to_string())?,
config.codex_home.as_path(),
)
let (role_config_toml, role_config_base) = if is_built_in {
let role_config_contents = built_in::config_file_contents(config_file)
.map(str::to_owned)
.ok_or_else(|| AGENT_TYPE_UNAVAILABLE_ERROR.to_string())?;
let role_config_toml: TomlValue = toml::from_str(&role_config_contents)
.map_err(|_| AGENT_TYPE_UNAVAILABLE_ERROR.to_string())?;
(role_config_toml, config.codex_home.as_path())
} else {
let role_config_contents = tokio::fs::read_to_string(config_file)
.await
.map_err(|_| AGENT_TYPE_UNAVAILABLE_ERROR.to_string())?;
let role_config_toml = parse_agent_role_file_contents(
&role_config_contents,
config_file,
config_file
.parent()
.ok_or_else(|| AGENT_TYPE_UNAVAILABLE_ERROR.to_string())?,
Some(role_name),
)
.map_err(|_| AGENT_TYPE_UNAVAILABLE_ERROR.to_string())?
.config;
(
tokio::fs::read_to_string(config_file)
.await
.map_err(|_| AGENT_TYPE_UNAVAILABLE_ERROR.to_string())?,
role_config_toml,
config_file
.parent()
.ok_or_else(|| AGENT_TYPE_UNAVAILABLE_ERROR.to_string())?,
)
};
let role_config_toml: TomlValue = toml::from_str(&role_config_contents)
.map_err(|_| AGENT_TYPE_UNAVAILABLE_ERROR.to_string())?;
deserialize_config_toml_with_base(role_config_toml.clone(), role_config_base)
.map_err(|_| AGENT_TYPE_UNAVAILABLE_ERROR.to_string())?;
let role_layer_toml = resolve_relative_paths_in_config_toml(role_config_toml, role_config_base)
@@ -391,6 +400,37 @@ mod tests {
assert_eq!(err, AGENT_TYPE_UNAVAILABLE_ERROR);
}
#[tokio::test]
async fn apply_role_ignores_agent_metadata_fields_in_user_role_file() {
let (home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
let role_path = write_role_config(
&home,
"metadata-role.toml",
r#"
name = "archivist"
description = "Role metadata"
nickname_candidates = ["Hypatia"]
developer_instructions = "Stay focused"
model = "role-model"
"#,
)
.await;
config.agent_roles.insert(
"custom".to_string(),
AgentRoleConfig {
description: None,
config_file: Some(role_path),
nickname_candidates: None,
},
);
apply_role_to_config(&mut config, Some("custom"))
.await
.expect("custom role should apply");
assert_eq!(config.model.as_deref(), Some("role-model"));
}
#[tokio::test]
async fn apply_role_preserves_unspecified_keys() {
let (home, mut config) = test_config_with_cli_overrides(vec![(
@@ -403,7 +443,7 @@ mod tests {
let role_path = write_role_config(
&home,
"effort-only.toml",
"model_reasoning_effort = \"high\"",
"developer_instructions = \"Stay focused\"\nmodel_reasoning_effort = \"high\"",
)
.await;
config.agent_roles.insert(
@@ -459,7 +499,12 @@ model_provider = "test-provider"
.build()
.await
.expect("load config");
let role_path = write_role_config(&home, "empty-role.toml", "").await;
let role_path = write_role_config(
&home,
"empty-role.toml",
"developer_instructions = \"Stay focused\"",
)
.await;
config.agent_roles.insert(
"custom".to_string(),
AgentRoleConfig {
@@ -515,8 +560,12 @@ model_provider = "role-provider"
.build()
.await
.expect("load config");
let role_path =
write_role_config(&home, "profile-role.toml", "profile = \"role-profile\"").await;
let role_path = write_role_config(
&home,
"profile-role.toml",
"developer_instructions = \"Stay focused\"\nprofile = \"role-profile\"",
)
.await;
config.agent_roles.insert(
"custom".to_string(),
AgentRoleConfig {
@@ -572,7 +621,7 @@ model_provider = "base-provider"
let role_path = write_role_config(
&home,
"provider-role.toml",
"model_provider = \"role-provider\"",
"developer_instructions = \"Stay focused\"\nmodel_provider = \"role-provider\"",
)
.await;
config.agent_roles.insert(
@@ -631,7 +680,9 @@ model_reasoning_effort = "low"
let role_path = write_role_config(
&home,
"profile-edit-role.toml",
r#"[profiles.base-profile]
r#"developer_instructions = "Stay focused"
[profiles.base-profile]
model_provider = "role-provider"
model_reasoning_effort = "high"
"#,
@@ -674,7 +725,9 @@ model_reasoning_effort = "high"
let role_path = write_role_config(
&home,
"sandbox-role.toml",
r#"[sandbox_workspace_write]
r#"developer_instructions = "Stay focused"
[sandbox_workspace_write]
writable_roots = ["./sandbox-root"]
"#,
)
@@ -732,7 +785,12 @@ writable_roots = ["./sandbox-root"]
)])
.await;
let before_layers = session_flags_layer_count(&config);
let role_path = write_role_config(&home, "model-role.toml", "model = \"role-model\"").await;
let role_path = write_role_config(
&home,
"model-role.toml",
"developer_instructions = \"Stay focused\"\nmodel = \"role-model\"",
)
.await;
config.agent_roles.insert(
"custom".to_string(),
AgentRoleConfig {
@@ -766,7 +824,9 @@ writable_roots = ["./sandbox-root"]
&home,
"skills-role.toml",
&format!(
r#"[[skills.config]]
r#"developer_instructions = "Stay focused"
[[skills.config]]
path = "{}"
enabled = false
"#,