refactor to use generalized subagent config builder

This commit is contained in:
Owen Lin
2026-04-09 13:29:21 -07:00
parent 8eb640f9c6
commit 21e7cbb9cd
11 changed files with 599 additions and 180 deletions
+5
View File
@@ -4,6 +4,7 @@ pub(crate) mod mailbox;
mod registry;
pub(crate) mod role;
pub(crate) mod status;
pub(crate) mod subagent_config;
pub(crate) use codex_protocol::protocol::AgentStatus;
pub(crate) use control::AgentControl;
@@ -12,3 +13,7 @@ pub(crate) use mailbox::MailboxReceiver;
pub(crate) use registry::exceeds_thread_spawn_depth_limit;
pub(crate) use registry::next_thread_spawn_depth;
pub(crate) use status::agent_status_from_event;
pub(crate) use subagent_config::SubAgentConfigBuilder;
pub(crate) use subagent_config::SubAgentExtensionInheritance;
pub(crate) use subagent_config::SubAgentPromptInheritance;
pub(crate) use subagent_config::SubAgentPromptSelection;
+320
View File
@@ -0,0 +1,320 @@
use anyhow::Context;
use codex_features::Feature;
use crate::codex::TurnContext;
use crate::config::Config;
use crate::config::Constrained;
use crate::config::InitialContextInclusions;
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub(crate) enum SubAgentPromptInheritance {
InheritParent,
None,
Select(SubAgentPromptSelection),
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct SubAgentPromptSelection {
pub(crate) model_update: bool,
pub(crate) base_instructions: bool,
pub(crate) user_instructions: bool,
pub(crate) project_docs: bool,
pub(crate) developer_instructions: bool,
pub(crate) separate_developer_instructions: bool,
pub(crate) compact_prompt: bool,
pub(crate) permissions: bool,
pub(crate) memory: bool,
pub(crate) collaboration: bool,
pub(crate) realtime: bool,
pub(crate) personality: bool,
pub(crate) apps: bool,
pub(crate) skills: bool,
pub(crate) plugins: bool,
pub(crate) commit: bool,
pub(crate) environment_context: bool,
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub(crate) enum SubAgentExtensionInheritance {
InheritParent,
None,
Select(SubAgentExtensionSelection),
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct SubAgentExtensionSelection {
pub(crate) mcp_servers: bool,
pub(crate) apps: bool,
pub(crate) plugins: bool,
pub(crate) tool_search: bool,
pub(crate) web_search: bool,
pub(crate) js_repl: bool,
pub(crate) code_mode: bool,
pub(crate) memory: bool,
pub(crate) apply_patch: bool,
pub(crate) subagents: bool,
pub(crate) image_generation: bool,
pub(crate) request_permissions: bool,
pub(crate) skill_dependencies: bool,
pub(crate) shell_snapshot: bool,
pub(crate) shell_zsh_fork: bool,
pub(crate) unified_exec: bool,
pub(crate) request_rule: bool,
pub(crate) git_commit: bool,
}
pub(crate) struct SubAgentConfigBuilder {
config: Config,
}
impl SubAgentConfigBuilder {
pub(crate) fn from_parent_config(parent_config: &Config) -> Self {
Self {
config: parent_config.clone(),
}
}
pub(crate) fn from_parent_turn(turn: &TurnContext) -> anyhow::Result<Self> {
let base_config = turn.config.clone();
let mut builder = Self::from_parent_config(base_config.as_ref());
builder.config.model = Some(turn.model_info.slug.clone());
builder.config.model_provider = turn.provider.clone();
builder.config.model_reasoning_effort = turn.reasoning_effort;
builder.config.model_reasoning_summary = Some(turn.reasoning_summary);
builder.config.developer_instructions = turn.developer_instructions.clone();
builder.config.compact_prompt = turn.compact_prompt.clone();
Self::apply_turn_runtime_to_config(&mut builder.config, turn)?;
Ok(builder)
}
pub(crate) fn apply_turn_runtime_to_config(
config: &mut Config,
turn: &TurnContext,
) -> anyhow::Result<()> {
config
.permissions
.approval_policy
.set(turn.approval_policy.value())
.context("approval_policy is invalid")?;
config.permissions.shell_environment_policy = turn.shell_environment_policy.clone();
config.codex_linux_sandbox_exe = turn.codex_linux_sandbox_exe.clone();
config.cwd = turn.cwd.clone();
config
.permissions
.sandbox_policy
.set(turn.sandbox_policy.get().clone())
.context("sandbox_policy is invalid")?;
config.permissions.file_system_sandbox_policy = turn.file_system_sandbox_policy.clone();
config.permissions.network_sandbox_policy = turn.network_sandbox_policy;
Ok(())
}
pub(crate) fn prompt_inheritance(mut self, inheritance: SubAgentPromptInheritance) -> Self {
match inheritance {
SubAgentPromptInheritance::InheritParent => {}
SubAgentPromptInheritance::None => {
self.config.base_instructions = None;
self.config.user_instructions = None;
self.config.developer_instructions = None;
self.config.compact_prompt = None;
self.config.project_doc_max_bytes = 0;
self.config.project_doc_fallback_filenames.clear();
self.config.include_permissions_instructions = false;
self.config.include_apps_instructions = false;
self.config.include_environment_context = false;
self.config.initial_context_inclusions = InitialContextInclusions::none();
}
SubAgentPromptInheritance::Select(selection) => {
self.apply_prompt_selection(selection);
}
}
self
}
pub(crate) fn extension_inheritance(
mut self,
inheritance: SubAgentExtensionInheritance,
) -> anyhow::Result<Self> {
match inheritance {
SubAgentExtensionInheritance::InheritParent => {}
SubAgentExtensionInheritance::None => {
self.apply_extension_selection(SubAgentExtensionSelection::none())?;
}
SubAgentExtensionInheritance::Select(selection) => {
self.apply_extension_selection(selection)?;
}
}
Ok(self)
}
pub(crate) fn initial_context_inclusions(
mut self,
inclusions: InitialContextInclusions,
) -> Self {
self.config.initial_context_inclusions = inclusions;
self
}
pub(crate) fn build(self) -> Config {
self.config
}
fn apply_prompt_selection(&mut self, selection: SubAgentPromptSelection) {
if !selection.base_instructions {
self.config.base_instructions = None;
}
if !selection.user_instructions {
self.config.user_instructions = None;
}
if !selection.developer_instructions {
self.config.developer_instructions = None;
}
if !selection.compact_prompt {
self.config.compact_prompt = None;
}
if !selection.project_docs {
self.config.project_doc_max_bytes = 0;
self.config.project_doc_fallback_filenames.clear();
}
self.config.include_permissions_instructions = selection.permissions;
self.config.include_apps_instructions = selection.apps;
self.config.include_environment_context = selection.environment_context;
self.config.initial_context_inclusions = InitialContextInclusions {
model_update: selection.model_update,
permissions: selection.permissions,
developer_instructions: selection.developer_instructions,
separate_developer_instructions: selection.separate_developer_instructions,
memory: selection.memory,
collaboration: selection.collaboration,
realtime: selection.realtime,
personality: selection.personality,
apps: selection.apps,
skills: selection.skills,
plugins: selection.plugins,
commit: selection.commit,
user_instructions: selection.user_instructions || selection.project_docs,
environment_context: selection.environment_context,
};
}
fn apply_extension_selection(
&mut self,
selection: SubAgentExtensionSelection,
) -> anyhow::Result<()> {
if !selection.mcp_servers {
self.config.mcp_servers = Constrained::allow_only(Default::default());
}
if !selection.apps {
self.disable_feature(Feature::Apps)?;
}
if !selection.plugins {
self.disable_feature(Feature::Plugins)?;
}
if !selection.tool_search {
self.disable_feature(Feature::ToolSearch)?;
self.disable_feature(Feature::ToolSuggest)?;
}
if !selection.web_search {
self.disable_feature(Feature::WebSearchRequest)?;
self.disable_feature(Feature::WebSearchCached)?;
}
if !selection.js_repl {
self.config.js_repl_node_path = None;
self.config.js_repl_node_module_dirs = Vec::new();
self.disable_feature(Feature::JsReplToolsOnly)?;
self.disable_feature(Feature::JsRepl)?;
}
if !selection.code_mode {
self.disable_feature(Feature::CodeModeOnly)?;
self.disable_feature(Feature::CodeMode)?;
}
if !selection.memory {
self.disable_feature(Feature::MemoryTool)?;
}
if !selection.apply_patch {
self.config.include_apply_patch_tool = false;
self.disable_feature(Feature::ApplyPatchFreeform)?;
}
if !selection.subagents {
self.disable_feature(Feature::SpawnCsv)?;
self.disable_feature(Feature::MultiAgentV2)?;
self.disable_feature(Feature::Collab)?;
self.disable_feature(Feature::ChildAgentsMd)?;
}
if !selection.image_generation {
self.disable_feature(Feature::ImageGeneration)?;
}
if !selection.request_permissions {
self.disable_feature(Feature::RequestPermissionsTool)?;
self.disable_feature(Feature::ExecPermissionApprovals)?;
}
if !selection.skill_dependencies {
self.disable_feature(Feature::SkillMcpDependencyInstall)?;
self.disable_feature(Feature::SkillEnvVarDependencyPrompt)?;
}
if !selection.shell_snapshot {
self.disable_feature(Feature::ShellSnapshot)?;
}
if !selection.shell_zsh_fork {
self.config.zsh_path = None;
self.disable_feature(Feature::ShellZshFork)?;
}
if !selection.unified_exec {
self.config.use_experimental_unified_exec_tool = false;
self.config.main_execve_wrapper_exe = None;
self.disable_feature(Feature::UnifiedExec)?;
}
if !selection.request_rule {
self.disable_feature(Feature::RequestRule)?;
}
if !selection.git_commit {
self.disable_feature(Feature::CodexGitCommit)?;
}
Ok(())
}
fn disable_feature(&mut self, feature: Feature) -> anyhow::Result<()> {
self.config.features.disable(feature).map_err(|err| {
anyhow::anyhow!(
"subagent config could not disable `features.{}`: {err}",
feature.key()
)
})?;
if self.config.features.enabled(feature) {
anyhow::bail!(
"subagent config requires `features.{}` to be disabled",
feature.key()
);
}
Ok(())
}
}
impl SubAgentExtensionSelection {
const fn none() -> Self {
Self {
mcp_servers: false,
apps: false,
plugins: false,
tool_search: false,
web_search: false,
js_repl: false,
code_mode: false,
memory: false,
apply_patch: false,
subagents: false,
image_generation: false,
request_permissions: false,
skill_dependencies: false,
shell_snapshot: false,
shell_zsh_fork: false,
unified_exec: false,
request_rule: false,
git_commit: false,
}
}
}
+82 -79
View File
@@ -3680,31 +3680,26 @@ impl Session {
let mut developer_sections = Vec::<String>::with_capacity(8);
let mut contextual_user_sections = Vec::<String>::with_capacity(2);
let shell = self.user_shell();
let (
reference_context_item,
previous_turn_settings,
collaboration_mode,
base_instructions,
session_source,
) = {
let (reference_context_item, previous_turn_settings, collaboration_mode, base_instructions) = {
let state = self.state.lock().await;
(
state.reference_context_item(),
state.previous_turn_settings(),
state.session_configuration.collaboration_mode.clone(),
state.session_configuration.base_instructions.clone(),
state.session_configuration.session_source.clone(),
)
};
if let Some(model_switch_message) =
crate::context_manager::updates::build_model_instructions_update_item(
previous_turn_settings.as_ref(),
turn_context,
)
let initial_context = turn_context.config.initial_context_inclusions;
if initial_context.model_update
&& let Some(model_switch_message) =
crate::context_manager::updates::build_model_instructions_update_item(
previous_turn_settings.as_ref(),
turn_context,
)
{
developer_sections.push(model_switch_message.into_text());
}
if turn_context.config.include_permissions_instructions {
if initial_context.permissions && turn_context.config.include_permissions_instructions {
developer_sections.push(
DeveloperInstructions::from_policy(
turn_context.sandbox_policy.get(),
@@ -3722,71 +3717,76 @@ impl Session {
.into_text(),
);
}
let separate_guardian_developer_message =
crate::guardian::is_guardian_reviewer_source(&session_source);
// Keep the guardian policy prompt out of the aggregated developer bundle so it
// stays isolated as its own top-level developer message for guardian subagents.
if !separate_guardian_developer_message
// Some internally spawned subagents keep their role policy separate from the
// aggregated developer bundle so their top-level instructions are easy to audit.
if initial_context.developer_instructions
&& !initial_context.separate_developer_instructions
&& let Some(developer_instructions) = turn_context.developer_instructions.as_deref()
{
developer_sections.push(developer_instructions.to_string());
}
if !separate_guardian_developer_message {
// Add developer instructions for memories.
if turn_context.features.enabled(Feature::MemoryTool)
&& turn_context.config.memories.use_memories
&& let Some(memory_prompt) =
build_memory_tool_developer_instructions(&turn_context.config.codex_home).await
{
developer_sections.push(memory_prompt);
}
// Add developer instructions from collaboration_mode if they exist and are non-empty
if let Some(collab_instructions) =
// Add developer instructions for memories.
if initial_context.memory
&& turn_context.features.enabled(Feature::MemoryTool)
&& turn_context.config.memories.use_memories
&& let Some(memory_prompt) =
build_memory_tool_developer_instructions(&turn_context.config.codex_home).await
{
developer_sections.push(memory_prompt);
}
// Add developer instructions from collaboration_mode if they exist and are non-empty
if initial_context.collaboration
&& let Some(collab_instructions) =
DeveloperInstructions::from_collaboration_mode(&collaboration_mode)
{
developer_sections.push(collab_instructions.into_text());
}
if let Some(realtime_update) =
{
developer_sections.push(collab_instructions.into_text());
}
if initial_context.realtime
&& let Some(realtime_update) =
crate::context_manager::updates::build_initial_realtime_item(
reference_context_item.as_ref(),
previous_turn_settings.as_ref(),
turn_context,
)
{
developer_sections.push(realtime_update.into_text());
}
if self.features.enabled(Feature::Personality)
&& let Some(personality) = turn_context.personality
{
let model_info = turn_context.model_info.clone();
let has_baked_personality = model_info.supports_personality()
&& base_instructions == model_info.get_model_instructions(Some(personality));
if !has_baked_personality
&& let Some(personality_message) =
crate::context_manager::updates::personality_message_for(
&model_info,
personality,
)
{
developer_sections.push(
DeveloperInstructions::personality_spec_message(personality_message)
.into_text(),
);
}
}
if turn_context.config.include_apps_instructions && turn_context.apps_enabled() {
let mcp_connection_manager = self.services.mcp_connection_manager.read().await;
let accessible_and_enabled_connectors =
connectors::list_accessible_and_enabled_connectors_from_manager(
&mcp_connection_manager,
&turn_context.config,
{
developer_sections.push(realtime_update.into_text());
}
if initial_context.personality
&& self.features.enabled(Feature::Personality)
&& let Some(personality) = turn_context.personality
{
let model_info = turn_context.model_info.clone();
let has_baked_personality = model_info.supports_personality()
&& base_instructions == model_info.get_model_instructions(Some(personality));
if !has_baked_personality
&& let Some(personality_message) =
crate::context_manager::updates::personality_message_for(
&model_info,
personality,
)
.await;
if let Some(apps_section) = render_apps_section(&accessible_and_enabled_connectors)
{
developer_sections.push(apps_section);
}
{
developer_sections.push(
DeveloperInstructions::personality_spec_message(personality_message)
.into_text(),
);
}
}
if initial_context.apps
&& turn_context.config.include_apps_instructions
&& turn_context.apps_enabled()
{
let mcp_connection_manager = self.services.mcp_connection_manager.read().await;
let accessible_and_enabled_connectors =
connectors::list_accessible_and_enabled_connectors_from_manager(
&mcp_connection_manager,
&turn_context.config,
)
.await;
if let Some(apps_section) = render_apps_section(&accessible_and_enabled_connectors) {
developer_sections.push(apps_section);
}
}
if initial_context.skills {
let implicit_skills = turn_context
.turn_skills
.outcome
@@ -3794,6 +3794,8 @@ impl Session {
if let Some(skills_section) = render_skills_section(&implicit_skills) {
developer_sections.push(skills_section);
}
}
if initial_context.plugins {
let loaded_plugins = self
.services
.plugins_manager
@@ -3803,15 +3805,16 @@ impl Session {
{
developer_sections.push(plugin_section);
}
if turn_context.features.enabled(Feature::CodexGitCommit)
&& let Some(commit_message_instruction) = commit_message_trailer_instruction(
turn_context.config.commit_attribution.as_deref(),
)
{
developer_sections.push(commit_message_instruction);
}
}
if !separate_guardian_developer_message
if initial_context.commit
&& turn_context.features.enabled(Feature::CodexGitCommit)
&& let Some(commit_message_instruction) = commit_message_trailer_instruction(
turn_context.config.commit_attribution.as_deref(),
)
{
developer_sections.push(commit_message_instruction);
}
if initial_context.user_instructions
&& let Some(user_instructions) = turn_context.user_instructions.as_deref()
{
contextual_user_sections.push(
@@ -3822,7 +3825,7 @@ impl Session {
.serialize_to_text(),
);
}
if turn_context.config.include_environment_context {
if initial_context.environment_context && turn_context.config.include_environment_context {
let subagents = self
.services
.agent_control
@@ -3846,9 +3849,9 @@ impl Session {
{
items.push(contextual_user_message);
}
// Emit the guardian policy prompt as a separate developer item so the guardian
// subagent sees a distinct, easy-to-audit instruction block.
if separate_guardian_developer_message
// Emit selected role policy as a separate developer item.
if initial_context.developer_instructions
&& initial_context.separate_developer_instructions
&& let Some(developer_instructions) = turn_context.developer_instructions.as_deref()
&& let Some(guardian_developer_message) =
crate::context_manager::updates::build_developer_update_item(vec![
+8 -2
View File
@@ -247,6 +247,13 @@ async fn process_compacted_history_preserves_separate_guardian_developer_message
}
turn_context.session_source = guardian_source;
turn_context.developer_instructions = Some(guardian_policy.clone());
let mut config = (*turn_context.config).clone();
config.initial_context_inclusions = crate::config::InitialContextInclusions {
developer_instructions: true,
separate_developer_instructions: true,
..crate::config::InitialContextInclusions::none()
};
turn_context.config = Arc::new(config);
let refreshed = crate::compact_remote::process_compacted_history(
&session,
@@ -290,8 +297,7 @@ async fn process_compacted_history_preserves_separate_guardian_developer_message
.iter()
.any(|message| message.contains("stale developer message"))
);
assert!(developer_messages.len() >= 2);
assert_eq!(developer_messages.last(), Some(&guardian_policy));
assert_eq!(developer_messages, vec![guardian_policy]);
}
#[tokio::test]
+4
View File
@@ -4562,6 +4562,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
include_permissions_instructions: true,
include_apps_instructions: true,
include_environment_context: true,
initial_context_inclusions: InitialContextInclusions::full(),
compact_prompt: None,
commit_attribution: None,
forced_chatgpt_workspace_id: None,
@@ -4709,6 +4710,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
include_permissions_instructions: true,
include_apps_instructions: true,
include_environment_context: true,
initial_context_inclusions: InitialContextInclusions::full(),
compact_prompt: None,
commit_attribution: None,
forced_chatgpt_workspace_id: None,
@@ -4854,6 +4856,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
include_permissions_instructions: true,
include_apps_instructions: true,
include_environment_context: true,
initial_context_inclusions: InitialContextInclusions::full(),
compact_prompt: None,
commit_attribution: None,
forced_chatgpt_workspace_id: None,
@@ -4985,6 +4988,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
include_permissions_instructions: true,
include_apps_instructions: true,
include_environment_context: true,
initial_context_inclusions: InitialContextInclusions::full(),
compact_prompt: None,
commit_attribution: None,
forced_chatgpt_workspace_id: None,
+65
View File
@@ -269,6 +269,12 @@ pub struct Config {
/// Whether to inject the `<environment_context>` user block.
pub include_environment_context: bool,
/// In-memory policy for which startup context blocks are model-visible.
///
/// This is not loaded from config.toml. It is used by internally spawned
/// subagents that need a narrower prompt than the parent session.
pub(crate) initial_context_inclusions: InitialContextInclusions,
/// Compact prompt override.
pub compact_prompt: Option<String>,
@@ -576,6 +582,64 @@ pub struct Config {
pub otel: codex_config::types::OtelConfig,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct InitialContextInclusions {
pub(crate) model_update: bool,
pub(crate) permissions: bool,
pub(crate) developer_instructions: bool,
pub(crate) separate_developer_instructions: bool,
pub(crate) memory: bool,
pub(crate) collaboration: bool,
pub(crate) realtime: bool,
pub(crate) personality: bool,
pub(crate) apps: bool,
pub(crate) skills: bool,
pub(crate) plugins: bool,
pub(crate) commit: bool,
pub(crate) user_instructions: bool,
pub(crate) environment_context: bool,
}
impl InitialContextInclusions {
pub(crate) const fn full() -> Self {
Self {
model_update: true,
permissions: true,
developer_instructions: true,
separate_developer_instructions: false,
memory: true,
collaboration: true,
realtime: true,
personality: true,
apps: true,
skills: true,
plugins: true,
commit: true,
user_instructions: true,
environment_context: true,
}
}
pub(crate) const fn none() -> Self {
Self {
model_update: false,
permissions: false,
developer_instructions: false,
separate_developer_instructions: false,
memory: false,
collaboration: false,
realtime: false,
personality: false,
apps: false,
skills: false,
plugins: false,
commit: false,
user_instructions: false,
environment_context: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MultiAgentV2Config {
pub usage_hint_enabled: bool,
@@ -2018,6 +2082,7 @@ impl Config {
include_permissions_instructions,
include_apps_instructions,
include_environment_context,
initial_context_inclusions: InitialContextInclusions::full(),
// The config.toml omits "_mode" because it's a config file. However, "_mode"
// is important in code to differentiate the mode from the store implementation.
cli_auth_credentials_store_mode: cfg.cli_auth_credentials_store.unwrap_or_default(),
+44 -54
View File
@@ -24,16 +24,20 @@ use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
use tracing::warn;
use crate::agent::SubAgentConfigBuilder;
use crate::agent::SubAgentExtensionInheritance;
use crate::agent::SubAgentPromptInheritance;
use crate::agent::SubAgentPromptSelection;
use crate::codex::Codex;
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::codex_delegate::run_codex_thread_interactive;
use crate::config::Config;
use crate::config::Constrained;
use crate::config::InitialContextInclusions;
use crate::config::NetworkProxySpec;
use crate::config::Permissions;
use crate::rollout::recorder::RolloutRecorder;
use codex_features::Feature;
use codex_model_provider_info::ModelProviderInfo;
use super::GUARDIAN_REVIEW_TIMEOUT;
@@ -47,32 +51,6 @@ const GUARDIAN_BASE_INSTRUCTIONS: &str = concat!(
"Follow the guardian developer policy. ",
"Return only the required final JSON."
);
const GUARDIAN_DISABLED_FEATURES: &[Feature] = &[
Feature::SpawnCsv,
Feature::MultiAgentV2,
Feature::Collab,
Feature::WebSearchRequest,
Feature::WebSearchCached,
Feature::CodeModeOnly,
Feature::CodeMode,
Feature::JsReplToolsOnly,
Feature::JsRepl,
Feature::MemoryTool,
Feature::ChildAgentsMd,
Feature::Apps,
Feature::ToolSearch,
Feature::ToolSuggest,
Feature::Plugins,
Feature::ImageGeneration,
Feature::RequestPermissionsTool,
Feature::ExecPermissionApprovals,
Feature::RequestRule,
Feature::ShellSnapshot,
Feature::ShellZshFork,
Feature::UnifiedExec,
Feature::ApplyPatchFreeform,
Feature::CodexGitCommit,
];
const GUARDIAN_FOLLOWUP_REVIEW_REMINDER: &str = concat!(
"Use prior reviews as context, not binding precedent. ",
"Follow the Workspace Policy. ",
@@ -644,7 +622,44 @@ pub(crate) fn build_guardian_review_session_config(
active_model: &str,
reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
) -> anyhow::Result<Config> {
let mut guardian_config = parent_config.clone();
let mut guardian_config = SubAgentConfigBuilder::from_parent_config(parent_config)
.prompt_inheritance(SubAgentPromptInheritance::Select(SubAgentPromptSelection {
model_update: false,
base_instructions: false,
user_instructions: false,
project_docs: true,
developer_instructions: true,
separate_developer_instructions: true,
compact_prompt: false,
permissions: false,
memory: false,
collaboration: false,
realtime: false,
personality: false,
apps: false,
skills: false,
plugins: false,
commit: false,
environment_context: true,
}))
.extension_inheritance(SubAgentExtensionInheritance::None)?
.initial_context_inclusions(InitialContextInclusions {
model_update: false,
permissions: false,
developer_instructions: true,
separate_developer_instructions: true,
memory: false,
collaboration: false,
realtime: false,
personality: false,
apps: false,
skills: false,
plugins: false,
commit: false,
user_instructions: true,
environment_context: true,
})
.build();
guardian_config.model = Some(active_model.to_string());
guardian_config.model_reasoning_effort = reasoning_effort;
guardian_config.personality = None;
@@ -660,13 +675,7 @@ pub(crate) fn build_guardian_review_session_config(
guardian_config.compact_prompt = None;
guardian_config.include_permissions_instructions = false;
guardian_config.include_apps_instructions = false;
guardian_config.include_environment_context = false;
guardian_config.project_doc_max_bytes = 0;
guardian_config.mcp_servers = Constrained::allow_only(Default::default());
guardian_config.js_repl_node_path = None;
guardian_config.js_repl_node_module_dirs = Vec::new();
guardian_config.zsh_path = None;
guardian_config.main_execve_wrapper_exe = None;
guardian_config.include_environment_context = true;
guardian_config.include_apply_patch_tool = false;
guardian_config.use_experimental_unified_exec_tool = false;
guardian_config.permissions.allow_login_shell = false;
@@ -688,28 +697,9 @@ pub(crate) fn build_guardian_review_session_config(
&SandboxPolicy::new_read_only_policy(),
)?);
}
disable_guardian_feature_set(&mut guardian_config)?;
Ok(guardian_config)
}
fn disable_guardian_feature_set(guardian_config: &mut Config) -> anyhow::Result<()> {
for &feature in GUARDIAN_DISABLED_FEATURES {
guardian_config.features.disable(feature).map_err(|err| {
anyhow::anyhow!(
"guardian review session could not disable `features.{}`: {err}",
feature.key()
)
})?;
if guardian_config.features.enabled(feature) {
anyhow::bail!(
"guardian review session requires `features.{}` to be disabled",
feature.key()
);
}
}
Ok(())
}
async fn run_before_review_deadline<T>(
deadline: tokio::time::Instant,
external_cancel: Option<&CancellationToken>,
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+42 -5
View File
@@ -4,6 +4,7 @@ use crate::codex::TurnContext;
use crate::config::Config;
use crate::config::ConfigOverrides;
use crate::config::Constrained;
use crate::config::InitialContextInclusions;
use crate::config::ManagedFeatures;
use crate::config::NetworkProxySpec;
use crate::config::test_config;
@@ -560,6 +561,10 @@ async fn guardian_review_request_layout_matches_model_visible_request_snapshot()
let (mut session, mut turn) = crate::codex::make_session_and_context().await;
let temp_cwd = TempDir::new()?;
std::fs::write(
temp_cwd.path().join("AGENTS.md"),
"GUARDIAN_REPO_AGENTS_SHOULD_BE_VISIBLE",
)?;
let mut config = (*turn.config).clone();
config.cwd = temp_cwd.abs();
config.model_provider.base_url = Some(format!("{}/v1", server.uri()));
@@ -624,14 +629,24 @@ async fn guardian_review_request_layout_matches_model_visible_request_snapshot()
"PARENT_BASE_INSTRUCTIONS_SHOULD_NOT_BE_VISIBLE",
"PARENT_USER_INSTRUCTIONS_SHOULD_NOT_BE_VISIBLE",
"PARENT_DEVELOPER_INSTRUCTIONS_SHOULD_NOT_BE_VISIBLE",
"<environment_context>",
"<permissions instructions>",
"<apps_instructions>",
"<skills_instructions>",
"<plugins_instructions>",
] {
assert!(
!request_body_text.contains(omitted_parent_text),
"guardian review request should omit inherited parent text: {omitted_parent_text}"
);
}
assert!(
request_body_text.contains("GUARDIAN_REPO_AGENTS_SHOULD_BE_VISIBLE"),
"guardian review request should include repo AGENTS.md project docs"
);
assert!(
request_body_text.contains("<environment_context>"),
"guardian review request should include existing environment context"
);
let mut settings = Settings::clone_current();
settings.set_snapshot_path("snapshots");
settings.set_prepend_module_to_snapshot(false);
@@ -1192,8 +1207,27 @@ fn guardian_review_session_config_strips_parent_prompt_and_tooling_surface() {
assert_eq!(guardian_config.compact_prompt, None);
assert!(!guardian_config.include_permissions_instructions);
assert!(!guardian_config.include_apps_instructions);
assert!(!guardian_config.include_environment_context);
assert_eq!(guardian_config.project_doc_max_bytes, 0);
assert!(guardian_config.include_environment_context);
assert_eq!(guardian_config.project_doc_max_bytes, 4096);
assert_eq!(
guardian_config.initial_context_inclusions,
InitialContextInclusions {
model_update: false,
permissions: false,
developer_instructions: true,
separate_developer_instructions: true,
memory: false,
collaboration: false,
realtime: false,
personality: false,
apps: false,
skills: false,
plugins: false,
commit: false,
user_instructions: true,
environment_context: true,
}
);
assert!(guardian_config.mcp_servers.is_empty());
assert_eq!(guardian_config.js_repl_node_path, None);
assert_eq!(
@@ -1211,7 +1245,9 @@ fn guardian_review_session_config_strips_parent_prompt_and_tooling_surface() {
Feature::MemoryTool,
Feature::Collab,
Feature::SpawnCsv,
Feature::ChildAgentsMd,
Feature::ApplyPatchFreeform,
Feature::CodexGitCommit,
] {
assert!(!guardian_config.features.enabled(disabled_feature));
}
@@ -1283,9 +1319,10 @@ fn guardian_review_session_config_rejects_pinned_collab_feature() {
)
.expect_err("guardian config should fail when collab is pinned on");
let err = err.to_string();
assert!(
err.to_string()
.contains("guardian review session requires `features.multi_agent` to be disabled")
err.contains("`features.multi_agent`"),
"error should name the pinned feature: {err}"
);
}
@@ -1,4 +1,7 @@
use crate::agent::AgentStatus;
use crate::agent::SubAgentConfigBuilder;
use crate::agent::SubAgentExtensionInheritance;
use crate::agent::SubAgentPromptInheritance;
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::config::Config;
@@ -221,17 +224,16 @@ pub(crate) fn build_agent_resume_config(
}
fn build_agent_shared_config(turn: &TurnContext) -> Result<Config, FunctionCallError> {
let base_config = turn.config.clone();
let mut config = (*base_config).clone();
config.model = Some(turn.model_info.slug.clone());
config.model_provider = turn.provider.clone();
config.model_reasoning_effort = turn.reasoning_effort;
config.model_reasoning_summary = Some(turn.reasoning_summary);
config.developer_instructions = turn.developer_instructions.clone();
config.compact_prompt = turn.compact_prompt.clone();
apply_spawn_agent_runtime_overrides(&mut config, turn)?;
Ok(config)
let builder = SubAgentConfigBuilder::from_parent_turn(turn).map_err(|err| {
FunctionCallError::RespondToModel(format!("spawn config is invalid: {err}"))
})?;
let builder = builder
.prompt_inheritance(SubAgentPromptInheritance::InheritParent)
.extension_inheritance(SubAgentExtensionInheritance::InheritParent)
.map_err(|err| {
FunctionCallError::RespondToModel(format!("spawn config is invalid: {err}"))
})?;
Ok(builder.build())
}
/// Copies runtime-only turn state onto a child config before it is handed to `AgentControl`.
@@ -242,26 +244,8 @@ pub(crate) fn apply_spawn_agent_runtime_overrides(
config: &mut Config,
turn: &TurnContext,
) -> Result<(), FunctionCallError> {
config
.permissions
.approval_policy
.set(turn.approval_policy.value())
.map_err(|err| {
FunctionCallError::RespondToModel(format!("approval_policy is invalid: {err}"))
})?;
config.permissions.shell_environment_policy = turn.shell_environment_policy.clone();
config.codex_linux_sandbox_exe = turn.codex_linux_sandbox_exe.clone();
config.cwd = turn.cwd.clone();
config
.permissions
.sandbox_policy
.set(turn.sandbox_policy.get().clone())
.map_err(|err| {
FunctionCallError::RespondToModel(format!("sandbox_policy is invalid: {err}"))
})?;
config.permissions.file_system_sandbox_policy = turn.file_system_sandbox_policy.clone();
config.permissions.network_sandbox_policy = turn.network_sandbox_policy;
Ok(())
SubAgentConfigBuilder::apply_turn_runtime_to_config(config, turn)
.map_err(|err| FunctionCallError::RespondToModel(format!("spawn config is invalid: {err}")))
}
pub(crate) fn apply_spawn_agent_overrides(config: &mut Config, child_depth: i32) {