Restrict agent roles to bounded configuration overrides (#39299)

## Why

Agent roles should customize a child agent without expanding the authority or changing the provider configuration inherited from its parent session.

## What changed

- Apply only supported role overrides for model behavior, developer instructions, personality, service tier, and capability reductions.
- Preserve parent-owned permissions, model providers, endpoints, MCP servers, notifications, and other unrestricted configuration.
- Keep managed feature requirements effective when a role disables capabilities, and reject symlinked user role files.
- Use the same bounded role application path for both multi-agent implementations and resumed agents.

## Testing

Add coverage for authority preservation, managed feature requirements, symlink rejection, provider inheritance, and provider routing after resume.

GitOrigin-RevId: c528d615b691f9c02bfbc21154d514ea07743010
This commit is contained in:
jif
2026-08-18 22:20:32 +00:00
committed by copyberry
parent 45528c5132
commit 1a6e07a4fe
6 changed files with 332 additions and 209 deletions
@@ -1,5 +1,4 @@
use crate::agent::role::apply_role_to_config;
use crate::agent::role::apply_role_to_config_for_multi_agent_v2;
use crate::config::Config;
use crate::config::DEFAULT_MULTI_AGENT_V2_MIN_WAIT_TIMEOUT_MS;
use crate::config::HARD_MAX_MULTI_AGENT_V2_TIMEOUT_MS;
@@ -389,15 +388,9 @@ pub(crate) async fn apply_spawn_agent_role(
) -> Result<(), FunctionCallError> {
let previous_model = config.model.clone();
let previous_reasoning_effort = config.model_reasoning_effort.clone();
if session.multi_agent_version() == Some(MultiAgentVersion::V2) {
apply_role_to_config_for_multi_agent_v2(config, role_name)
.await
.map_err(FunctionCallError::RespondToModel)?;
} else {
apply_role_to_config(config, role_name)
.await
.map_err(FunctionCallError::RespondToModel)?;
}
apply_role_to_config(config, role_name)
.await
.map_err(FunctionCallError::RespondToModel)?;
if config.model == previous_model && config.model_reasoning_effort == previous_reasoning_effort
{
return Ok(());
@@ -919,6 +919,7 @@ async fn multi_agent_v2_spawn_partial_fork_turns_allows_agent_type_override() {
multi_agent_version: codex_protocol::protocol::MultiAgentVersion::V2,
..turn
};
let parent_provider_id = turn.config.model_provider_id.clone();
let output = SpawnAgentHandlerV2::default()
.handle(invocation(
@@ -952,7 +953,7 @@ async fn multi_agent_v2_spawn_partial_fork_turns_allows_agent_type_override() {
.await;
assert_eq!(snapshot.model, "gpt-5-role-override");
assert_eq!(snapshot.model_provider_id, "ollama");
assert_eq!(snapshot.model_provider_id, parent_provider_id);
assert_eq!(snapshot.reasoning_effort, Some(ReasoningEffort::Minimal));
}