[codex] allow disabling prompt instruction blocks (#16735)

This PR adds root and profile config switches to omit the generated
`<permissions instructions>` and `<apps_instructions>` prompt blocks
while keeping both enabled by default, and it gates both the initial
developer-context injection and later permissions diff injection so
turning the permissions block off stays effective across turn-context
overrides.

Also added a prompt debug tool that can be used as `codex debug
prompt-input "hello"` and dumps the constructed items list.
This commit is contained in:
Thibault Sottiaux
2026-04-03 23:47:56 +00:00
committed by GitHub
parent f263607c60
commit 8d19646861
11 changed files with 531 additions and 120 deletions
+20 -17
View File
@@ -3578,22 +3578,24 @@ impl Session {
{
developer_sections.push(model_switch_message.into_text());
}
developer_sections.push(
DeveloperInstructions::from_policy(
turn_context.sandbox_policy.get(),
turn_context.approval_policy.value(),
turn_context.config.approvals_reviewer,
self.services.exec_policy.current().as_ref(),
&turn_context.cwd,
turn_context
.features
.enabled(Feature::ExecPermissionApprovals),
turn_context
.features
.enabled(Feature::RequestPermissionsTool),
)
.into_text(),
);
if turn_context.config.include_permissions_instructions {
developer_sections.push(
DeveloperInstructions::from_policy(
turn_context.sandbox_policy.get(),
turn_context.approval_policy.value(),
turn_context.config.approvals_reviewer,
self.services.exec_policy.current().as_ref(),
&turn_context.cwd,
turn_context
.features
.enabled(Feature::ExecPermissionApprovals),
turn_context
.features
.enabled(Feature::RequestPermissionsTool),
)
.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
@@ -3643,7 +3645,7 @@ impl Session {
);
}
}
if turn_context.apps_enabled() {
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(
@@ -6485,6 +6487,7 @@ pub(crate) fn build_prompt(
output_schema: turn_context.final_output_json_schema.clone(),
}
}
#[allow(clippy::too_many_arguments)]
#[instrument(level = "trace",
skip_all,
+34
View File
@@ -4493,6 +4493,8 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
base_instructions: None,
developer_instructions: None,
guardian_developer_instructions: None,
include_permissions_instructions: true,
include_apps_instructions: true,
compact_prompt: None,
commit_attribution: None,
forced_chatgpt_workspace_id: None,
@@ -4635,6 +4637,8 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
base_instructions: None,
developer_instructions: None,
guardian_developer_instructions: None,
include_permissions_instructions: true,
include_apps_instructions: true,
compact_prompt: None,
commit_attribution: None,
forced_chatgpt_workspace_id: None,
@@ -4775,6 +4779,8 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
base_instructions: None,
developer_instructions: None,
guardian_developer_instructions: None,
include_permissions_instructions: true,
include_apps_instructions: true,
compact_prompt: None,
commit_attribution: None,
forced_chatgpt_workspace_id: None,
@@ -4901,6 +4907,8 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
base_instructions: None,
developer_instructions: None,
guardian_developer_instructions: None,
include_permissions_instructions: true,
include_apps_instructions: true,
compact_prompt: None,
commit_attribution: None,
forced_chatgpt_workspace_id: None,
@@ -5783,6 +5791,32 @@ async fn approvals_reviewer_defaults_to_manual_only_without_guardian_feature() -
Ok(())
}
#[tokio::test]
async fn prompt_instruction_blocks_can_be_disabled_from_config_and_profiles() -> std::io::Result<()>
{
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"include_permissions_instructions = false
include_apps_instructions = false
profile = "chatty"
[profiles.chatty]
include_permissions_instructions = true
"#,
)?;
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.build()
.await?;
assert!(config.include_permissions_instructions);
assert!(!config.include_apps_instructions);
Ok(())
}
#[tokio::test]
async fn approvals_reviewer_stays_manual_only_when_guardian_feature_is_enabled()
-> std::io::Result<()> {
+22
View File
@@ -280,6 +280,12 @@ pub struct Config {
/// Guardian-specific developer instructions override from requirements.toml.
pub guardian_developer_instructions: Option<String>,
/// Whether to inject the `<permissions instructions>` developer block.
pub include_permissions_instructions: bool,
/// Whether to inject the `<apps_instructions>` developer block.
pub include_apps_instructions: bool,
/// Compact prompt override.
pub compact_prompt: Option<String>,
@@ -1183,6 +1189,12 @@ pub struct ConfigToml {
#[serde(default)]
pub developer_instructions: Option<String>,
/// Whether to inject the `<permissions instructions>` developer block.
pub include_permissions_instructions: Option<bool>,
/// Whether to inject the `<apps_instructions>` developer block.
pub include_apps_instructions: Option<bool>,
/// Optional path to a file containing model instructions that will override
/// the built-in instructions for the selected model. Users are STRONGLY
/// DISCOURAGED from using this field, as deviating from the instructions
@@ -2452,6 +2464,14 @@ impl Config {
Self::try_read_non_empty_file(model_instructions_path, "model instructions file")?;
let base_instructions = base_instructions.or(file_base_instructions);
let developer_instructions = developer_instructions.or(cfg.developer_instructions);
let include_permissions_instructions = config_profile
.include_permissions_instructions
.or(cfg.include_permissions_instructions)
.unwrap_or(true);
let include_apps_instructions = config_profile
.include_apps_instructions
.or(cfg.include_apps_instructions)
.unwrap_or(true);
let guardian_developer_instructions = guardian_developer_instructions_from_requirements(
config_layer_stack.requirements_toml(),
);
@@ -2618,6 +2638,8 @@ impl Config {
developer_instructions,
compact_prompt,
commit_attribution,
include_permissions_instructions,
include_apps_instructions,
// 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(),
+2
View File
@@ -49,6 +49,8 @@ pub struct ConfigProfile {
pub experimental_instructions_file: Option<AbsolutePathBuf>,
pub experimental_compact_prompt_file: Option<AbsolutePathBuf>,
pub include_apply_patch_tool: Option<bool>,
pub include_permissions_instructions: Option<bool>,
pub include_apps_instructions: Option<bool>,
pub experimental_use_unified_exec_tool: Option<bool>,
pub experimental_use_freeform_apply_patch: Option<bool>,
pub tools_view_image: Option<bool>,
@@ -33,6 +33,10 @@ fn build_permissions_update_item(
next: &TurnContext,
exec_policy: &Policy,
) -> Option<DeveloperInstructions> {
if !next.config.include_permissions_instructions {
return None;
}
let prev = previous?;
if prev.sandbox_policy == *next.sandbox_policy.get()
&& prev.approval_policy == next.approval_policy.value()
+2
View File
@@ -58,6 +58,8 @@ pub mod utils;
pub use utils::path_utils;
pub mod personality_migration;
pub mod plugins;
#[doc(hidden)]
pub mod prompt_debug;
pub(crate) mod mentions {
pub(crate) use crate::plugins::build_connector_slug_counts;
pub(crate) use crate::plugins::build_skill_name_counts;
+152
View File
@@ -0,0 +1,152 @@
use std::collections::HashSet;
use std::sync::Arc;
use codex_exec_server::EnvironmentManager;
use codex_features::Feature;
use codex_login::AuthManager;
use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::SessionSource;
use codex_protocol::user_input::UserInput;
use tokio_util::sync::CancellationToken;
use crate::codex::Session;
use crate::codex::build_prompt;
use crate::codex::built_tools;
use crate::config::Config;
use crate::thread_manager::ThreadManager;
/// Build the model-visible `input` list for a single debug turn.
#[doc(hidden)]
pub async fn build_prompt_input(
mut config: Config,
input: Vec<UserInput>,
) -> CodexResult<Vec<ResponseItem>> {
config.ephemeral = true;
let auth_manager = AuthManager::shared(
config.codex_home.clone(),
/*enable_codex_api_key_env*/ false,
config.cli_auth_credentials_store_mode,
);
auth_manager.set_forced_chatgpt_workspace_id(config.forced_chatgpt_workspace_id.clone());
let thread_manager = ThreadManager::new(
&config,
Arc::clone(&auth_manager),
SessionSource::Exec,
CollaborationModesConfig {
default_mode_request_user_input: config
.features
.enabled(Feature::DefaultModeRequestUserInput),
},
Arc::new(EnvironmentManager::from_env()),
);
let thread = thread_manager.start_thread(config).await?;
let output = build_prompt_input_from_session(thread.thread.codex.session.as_ref(), input).await;
let shutdown = thread.thread.shutdown_and_wait().await;
let _removed = thread_manager.remove_thread(&thread.thread_id).await;
shutdown?;
output
}
pub(crate) async fn build_prompt_input_from_session(
sess: &Session,
input: Vec<UserInput>,
) -> CodexResult<Vec<ResponseItem>> {
let turn_context = sess.new_default_turn().await;
sess.record_context_updates_and_set_reference_context_item(turn_context.as_ref())
.await;
if !input.is_empty() {
let input_item = ResponseInputItem::from(input);
let response_item = ResponseItem::from(input_item);
sess.record_conversation_items(turn_context.as_ref(), std::slice::from_ref(&response_item))
.await;
}
let prompt_input = sess
.clone_history()
.await
.for_prompt(&turn_context.model_info.input_modalities);
let router = built_tools(
sess,
turn_context.as_ref(),
&prompt_input,
&HashSet::new(),
Some(turn_context.turn_skills.outcome.as_ref()),
&CancellationToken::new(),
)
.await?;
let base_instructions = sess.get_base_instructions().await;
let prompt = build_prompt(
prompt_input,
router.as_ref(),
turn_context.as_ref(),
base_instructions,
);
Ok(prompt.get_formatted_input())
}
#[cfg(test)]
mod tests {
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use crate::config::test_config;
use super::build_prompt_input;
#[tokio::test]
async fn build_prompt_input_includes_context_and_user_message() {
let codex_home = tempfile::tempdir().expect("create codex home");
let cwd = tempfile::tempdir().expect("create cwd");
let mut config = test_config();
config.codex_home = codex_home.path().to_path_buf();
config.cwd = AbsolutePathBuf::try_from(cwd.path().to_path_buf()).expect("absolute cwd");
config.user_instructions = Some("Project-specific test instructions".to_string());
let input = build_prompt_input(
config,
vec![UserInput::Text {
text: "hello from debug prompt".to_string(),
text_elements: Vec::new(),
}],
)
.await
.expect("build prompt input");
let expected_user_message = ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "hello from debug prompt".to_string(),
}],
end_turn: None,
phase: None,
};
assert_eq!(input.last(), Some(&expected_user_message));
assert!(input.iter().any(|item| {
let ResponseItem::Message { content, .. } = item else {
return false;
};
content.iter().any(|content_item| {
let (ContentItem::InputText { text } | ContentItem::OutputText { text }) =
content_item
else {
return false;
};
text.contains("Project-specific test instructions")
})
}));
}
}