[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 13:47:56 -10:00
committed by GitHub
parent f263607c60
commit 8d19646861
11 changed files with 531 additions and 120 deletions

View File

@@ -51,6 +51,8 @@ use codex_core::config::find_codex_home;
use codex_features::FEATURES;
use codex_features::Stage;
use codex_features::is_known_feature_key;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::user_input::UserInput;
use codex_terminal_detection::TerminalName;
/// Codex CLI
@@ -170,6 +172,9 @@ enum DebugSubcommand {
/// Tooling: helps debug the app server.
AppServer(DebugAppServerCommand),
/// Render the model-visible prompt input list as JSON.
PromptInput(DebugPromptInputCommand),
/// Internal: reset local memory state for a fresh start.
#[clap(hide = true)]
ClearMemories,
@@ -193,6 +198,17 @@ struct DebugAppServerSendMessageV2Command {
user_message: String,
}
#[derive(Debug, Parser)]
struct DebugPromptInputCommand {
/// Optional user prompt to append after session context.
#[arg(value_name = "PROMPT")]
prompt: Option<String>,
/// Optional image(s) to attach to the user prompt.
#[arg(long = "image", short = 'i', value_name = "FILE", value_delimiter = ',', num_args = 1..)]
images: Vec<PathBuf>,
}
#[derive(Debug, Parser)]
struct ResumeCommand {
/// Conversation/session id (UUID) or thread name. UUIDs take precedence if it parses.
@@ -915,6 +931,20 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
)?;
run_debug_app_server_command(cmd).await?;
}
DebugSubcommand::PromptInput(cmd) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"debug prompt-input",
)?;
run_debug_prompt_input_command(
cmd,
root_config_overrides,
interactive,
arg0_paths.clone(),
)
.await?;
}
DebugSubcommand::ClearMemories => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
@@ -1083,6 +1113,72 @@ fn maybe_print_under_development_feature_warning(
);
}
async fn run_debug_prompt_input_command(
cmd: DebugPromptInputCommand,
root_config_overrides: CliConfigOverrides,
interactive: TuiCli,
arg0_paths: Arg0DispatchPaths,
) -> anyhow::Result<()> {
let mut cli_kv_overrides = root_config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
if interactive.web_search {
cli_kv_overrides.push((
"web_search".to_string(),
toml::Value::String("live".to_string()),
));
}
let approval_policy = if interactive.full_auto {
Some(AskForApproval::OnRequest)
} else if interactive.dangerously_bypass_approvals_and_sandbox {
Some(AskForApproval::Never)
} else {
interactive.approval_policy.map(Into::into)
};
let sandbox_mode = if interactive.full_auto {
Some(codex_protocol::config_types::SandboxMode::WorkspaceWrite)
} else if interactive.dangerously_bypass_approvals_and_sandbox {
Some(codex_protocol::config_types::SandboxMode::DangerFullAccess)
} else {
interactive.sandbox_mode.map(Into::into)
};
let overrides = ConfigOverrides {
model: interactive.model,
config_profile: interactive.config_profile,
approval_policy,
sandbox_mode,
cwd: interactive.cwd,
codex_self_exe: arg0_paths.codex_self_exe,
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe,
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe,
show_raw_agent_reasoning: interactive.oss.then_some(true),
ephemeral: Some(true),
additional_writable_roots: interactive.add_dir,
..Default::default()
};
let config =
Config::load_with_cli_overrides_and_harness_overrides(cli_kv_overrides, overrides).await?;
let mut input = interactive
.images
.into_iter()
.chain(cmd.images)
.map(|path| UserInput::LocalImage { path })
.collect::<Vec<_>>();
if let Some(prompt) = cmd.prompt.or(interactive.prompt) {
input.push(UserInput::Text {
text: prompt.replace("\r\n", "\n").replace('\r', "\n"),
text_elements: Vec::new(),
});
}
let prompt_input = codex_core::prompt_debug::build_prompt_input(config, input).await?;
println!("{}", serde_json::to_string_pretty(&prompt_input)?);
Ok(())
}
async fn run_debug_clear_memories_command(
root_config_overrides: &CliConfigOverrides,
interactive: &TuiCli,
@@ -1489,6 +1585,32 @@ mod tests {
app_server
}
#[test]
fn debug_prompt_input_parses_prompt_and_images() {
let cli = MultitoolCli::try_parse_from([
"codex",
"debug",
"prompt-input",
"hello",
"--image",
"/tmp/a.png,/tmp/b.png",
])
.expect("parse");
let Some(Subcommand::Debug(DebugCommand {
subcommand: DebugSubcommand::PromptInput(cmd),
})) = cli.subcommand
else {
panic!("expected debug prompt-input subcommand");
};
assert_eq!(cmd.prompt.as_deref(), Some("hello"));
assert_eq!(
cmd.images,
vec![PathBuf::from("/tmp/a.png"), PathBuf::from("/tmp/b.png")]
);
}
fn sample_exit_info(conversation_id: Option<&str>, thread_name: Option<&str>) -> AppExitInfo {
let token_usage = TokenUsage {
output_tokens: 2,