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
2 changes: 1 addition & 1 deletion codex-rs/core/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"description": "Path to a role-specific config layer. Relative paths are resolved relative to the `config.toml` that defines them."
},
"description": {
"description": "Human-facing role documentation used in spawn tool guidance.",
"description": "Human-facing role documentation used in spawn tool guidance. Required unless supplied by the referenced agent role file.",
"type": "string"
},
"nickname_candidates": {
Expand Down
102 changes: 81 additions & 21 deletions codex-rs/core/src/agent/role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
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())?,
config.codex_home.as_path(),
Some(role_name),
)
} else {
.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)
Expand Down Expand Up @@ -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![(
Expand All @@ -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(
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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"
"#,
Expand Down Expand Up @@ -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"]
"#,
)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
"#,
Expand Down
Loading
Loading