Split DeveloperInstructions into individual fragments. (#18813)

Split DeveloperInstructions into individual fragments.
This commit is contained in:
pakrym-oai
2026-04-21 10:22:36 -07:00
committed by GitHub
Unverified
parent 5fe767e8e1
commit 2a226096f6
55 changed files with 1410 additions and 1058 deletions
@@ -10,3 +10,4 @@ Codex maintains a context (history of messages) that is sent to the model in inf
3. No unbounded items - everything injected in the model context must have a bounded size and a hard cap.
4. No items larger than 10K tokens.
5. Highlight new individual items that can cross >1k tokens as P0. These need an additional manual review.
6. All injected fragments must be defined as structs in `core/context` and implement ContextualUserFragment trait
+2 -2
View File
@@ -22,8 +22,8 @@ pub use model::SkillLoadOutcome;
pub use model::SkillMetadata;
pub use model::SkillPolicy;
pub use model::filter_skill_load_outcome_for_product;
pub use render::RenderedSkillsSection;
pub use render::AvailableSkills;
pub use render::SkillMetadataBudget;
pub use render::SkillRenderReport;
pub use render::build_available_skills;
pub use render::default_skill_metadata_budget;
pub use render::render_skills_section;
+16 -47
View File
@@ -3,8 +3,6 @@ use codex_otel::SessionTelemetry;
use codex_otel::THREAD_SKILLS_ENABLED_TOTAL_METRIC;
use codex_otel::THREAD_SKILLS_KEPT_TOTAL_METRIC;
use codex_otel::THREAD_SKILLS_TRUNCATED_METRIC;
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_CLOSE_TAG;
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG;
use codex_protocol::protocol::SkillScope;
use codex_utils_output_truncation::approx_token_count;
@@ -48,8 +46,8 @@ pub enum SkillRenderSideEffects<'a> {
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenderedSkillsSection {
pub text: String,
pub struct AvailableSkills {
pub skill_lines: Vec<String>,
pub report: SkillRenderReport,
pub emit_warning: bool,
}
@@ -71,11 +69,11 @@ pub fn default_skill_metadata_budget(context_window: Option<i64>) -> SkillMetada
))
}
pub fn render_skills_section(
pub fn build_available_skills(
skills: &[SkillMetadata],
budget: SkillMetadataBudget,
side_effects: SkillRenderSideEffects<'_>,
) -> Option<RenderedSkillsSection> {
) -> Option<AvailableSkills> {
if skills.is_empty() {
let _ = record_skill_render_side_effects(
side_effects,
@@ -93,39 +91,8 @@ pub fn render_skills_section(
report.included_count,
report.omitted_count > 0,
);
let mut lines: Vec<String> = Vec::new();
lines.push("## Skills".to_string());
lines.push("A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.".to_string());
lines.push("### Available skills".to_string());
if !skill_lines.is_empty() {
lines.extend(skill_lines);
}
lines.push("### How to use skills".to_string());
lines.push(
r###"- Discovery: The list above is the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.
- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
- How to use a skill (progressive disclosure):
1) After deciding to use a skill, open its `SKILL.md`. Read only enough to follow the workflow.
2) When `SKILL.md` references relative paths (e.g., `scripts/foo.py`), resolve them relative to the skill directory listed above first, and only consider other paths if needed.
3) If `SKILL.md` points to extra folders such as `references/`, load only the specific files needed for the request; don't bulk-load everything.
4) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.
5) If `assets/` or templates exist, reuse them instead of recreating from scratch.
- Coordination and sequencing:
- If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
- Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
- Context hygiene:
- Keep context small: summarize long sections instead of pasting them; only load extra files when needed.
- Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
- When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue."###
.to_string(),
);
let body = lines.join("\n");
Some(RenderedSkillsSection {
text: format!("{SKILLS_INSTRUCTIONS_OPEN_TAG}\n{body}\n{SKILLS_INSTRUCTIONS_CLOSE_TAG}"),
Some(AvailableSkills {
skill_lines,
report,
emit_warning,
})
@@ -274,7 +241,7 @@ mod tests {
.cost(&format!("{}\n", render_skill_line(&admin)));
let budget = SkillMetadataBudget::Characters(system_cost + admin_cost);
let rendered = render_skills_section(
let rendered = build_available_skills(
&[system, user, repo, admin],
budget,
SkillRenderSideEffects::None,
@@ -284,10 +251,11 @@ mod tests {
assert_eq!(rendered.report.included_count, 2);
assert_eq!(rendered.report.omitted_count, 2);
assert!(!rendered.emit_warning);
assert!(rendered.text.contains("- system-skill:"));
assert!(rendered.text.contains("- admin-skill:"));
assert!(!rendered.text.contains("- repo-skill:"));
assert!(!rendered.text.contains("- user-skill:"));
let rendered_text = rendered.skill_lines.join("\n");
assert!(rendered_text.contains("- system-skill:"));
assert!(rendered_text.contains("- admin-skill:"));
assert!(!rendered_text.contains("- repo-skill:"));
assert!(!rendered_text.contains("- user-skill:"));
}
#[test]
@@ -300,13 +268,14 @@ mod tests {
let budget = SkillMetadataBudget::Characters(repo_cost);
let rendered =
render_skills_section(&[oversized, repo], budget, SkillRenderSideEffects::None)
build_available_skills(&[oversized, repo], budget, SkillRenderSideEffects::None)
.expect("skills render");
assert_eq!(rendered.report.included_count, 1);
assert_eq!(rendered.report.omitted_count, 1);
assert!(!rendered.emit_warning);
assert!(!rendered.text.contains("- oversized-system-skill:"));
assert!(rendered.text.contains("- repo-skill:"));
let rendered_text = rendered.skill_lines.join("\n");
assert!(!rendered_text.contains("- oversized-system-skill:"));
assert!(rendered_text.contains("- repo-skill:"));
}
}
+1 -2
View File
@@ -1,3 +1,2 @@
#[cfg(test)]
mod render;
pub(crate) use render::render_apps_section;
+3 -14
View File
@@ -1,22 +1,11 @@
use crate::context::AppsInstructions;
use crate::context::ContextualUserFragment;
use codex_app_server_protocol::AppInfo;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_protocol::protocol::APPS_INSTRUCTIONS_CLOSE_TAG;
use codex_protocol::protocol::APPS_INSTRUCTIONS_OPEN_TAG;
pub(crate) fn render_apps_section(connectors: &[AppInfo]) -> Option<String> {
if !connectors
.iter()
.any(|connector| connector.is_accessible && connector.is_enabled)
{
return None;
}
let body = format!(
"## Apps (Connectors)\nApps (Connectors) can be explicitly triggered in user messages in the format `[$app-name](app://{{connector_id}})`. Apps can also be implicitly triggered as long as the context suggests usage of available apps.\nAn app is equivalent to a set of MCP tools within the `{CODEX_APPS_MCP_SERVER_NAME}` MCP.\nAn installed app's MCP tools are either provided to you already, or can be lazy-loaded through the `tool_search` tool. If `tool_search` is available, the apps that are searchable by `tools_search` will be listed by it.\nDo not additionally call list_mcp_resources or list_mcp_resource_templates for apps."
);
Some(format!(
"{APPS_INSTRUCTIONS_OPEN_TAG}\n{body}\n{APPS_INSTRUCTIONS_CLOSE_TAG}"
))
AppsInstructions::from_connectors(connectors).map(|instructions| instructions.render())
}
#[cfg(test)]
@@ -0,0 +1,24 @@
use super::ContextualUserFragment;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ApprovedCommandPrefixSaved {
prefixes: String,
}
impl ApprovedCommandPrefixSaved {
pub(crate) fn new(prefixes: impl Into<String>) -> Self {
Self {
prefixes: prefixes.into(),
}
}
}
impl ContextualUserFragment for ApprovedCommandPrefixSaved {
const ROLE: &'static str = "developer";
const START_MARKER: &'static str = "";
const END_MARKER: &'static str = "";
fn body(&self) -> String {
format!("Approved command prefix saved:\n{}", self.prefixes)
}
}
@@ -0,0 +1,30 @@
use codex_app_server_protocol::AppInfo;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_protocol::protocol::APPS_INSTRUCTIONS_CLOSE_TAG;
use codex_protocol::protocol::APPS_INSTRUCTIONS_OPEN_TAG;
use super::ContextualUserFragment;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct AppsInstructions;
impl AppsInstructions {
pub(crate) fn from_connectors(connectors: &[AppInfo]) -> Option<Self> {
connectors
.iter()
.any(|connector| connector.is_accessible && connector.is_enabled)
.then_some(Self)
}
}
impl ContextualUserFragment for AppsInstructions {
const ROLE: &'static str = "developer";
const START_MARKER: &'static str = APPS_INSTRUCTIONS_OPEN_TAG;
const END_MARKER: &'static str = APPS_INSTRUCTIONS_CLOSE_TAG;
fn body(&self) -> String {
format!(
"\n## Apps (Connectors)\nApps (Connectors) can be explicitly triggered in user messages in the format `[$app-name](app://{{connector_id}})`. Apps can also be implicitly triggered as long as the context suggests usage of available apps.\nAn app is equivalent to a set of MCP tools within the `{CODEX_APPS_MCP_SERVER_NAME}` MCP.\nAn installed app's MCP tools are either provided to you already, or can be lazy-loaded through the `tool_search` tool. If `tool_search` is available, the apps that are searchable by `tools_search` will be listed by it.\nDo not additionally call list_mcp_resources or list_mcp_resource_templates for apps.\n"
)
}
}
@@ -0,0 +1,58 @@
use codex_plugin::PluginCapabilitySummary;
use codex_protocol::protocol::PLUGINS_INSTRUCTIONS_CLOSE_TAG;
use codex_protocol::protocol::PLUGINS_INSTRUCTIONS_OPEN_TAG;
use super::ContextualUserFragment;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct AvailablePluginsInstructions {
plugins: Vec<PluginCapabilitySummary>,
}
impl AvailablePluginsInstructions {
pub(crate) fn from_plugins(plugins: &[PluginCapabilitySummary]) -> Option<Self> {
if plugins.is_empty() {
return None;
}
Some(Self {
plugins: plugins.to_vec(),
})
}
}
impl ContextualUserFragment for AvailablePluginsInstructions {
const ROLE: &'static str = "developer";
const START_MARKER: &'static str = PLUGINS_INSTRUCTIONS_OPEN_TAG;
const END_MARKER: &'static str = PLUGINS_INSTRUCTIONS_CLOSE_TAG;
fn body(&self) -> String {
let mut lines = vec![
"## Plugins".to_string(),
"A plugin is a local bundle of skills, MCP servers, and apps. Below is the list of plugins that are enabled and available in this session.".to_string(),
"### Available plugins".to_string(),
];
lines.extend(
self.plugins
.iter()
.map(|plugin| match plugin.description.as_deref() {
Some(description) => format!("- `{}`: {description}", plugin.display_name),
None => format!("- `{}`", plugin.display_name),
}),
);
lines.push("### How to use plugins".to_string());
lines.push(
r###"- Discovery: The list above is the plugins available in this session.
- Skill naming: If a plugin contributes skills, those skill entries are prefixed with `plugin_name:` in the Skills list.
- Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn.
- Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task.
- Preference: When a relevant plugin is available, prefer using capabilities associated with that plugin over standalone capabilities that provide similar functionality.
- Missing/blocked: If the user requests a plugin that is not listed above, or the plugin does not have relevant callable capabilities for the task, say so briefly and continue with the best fallback."###
.to_string(),
);
format!("\n{}\n", lines.join("\n"))
}
}
@@ -0,0 +1,56 @@
use codex_core_skills::AvailableSkills;
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_CLOSE_TAG;
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG;
use super::ContextualUserFragment;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AvailableSkillsInstructions {
skill_lines: Vec<String>,
}
impl From<AvailableSkills> for AvailableSkillsInstructions {
fn from(available_skills: AvailableSkills) -> Self {
Self {
skill_lines: available_skills.skill_lines,
}
}
}
impl ContextualUserFragment for AvailableSkillsInstructions {
const ROLE: &'static str = "developer";
const START_MARKER: &'static str = SKILLS_INSTRUCTIONS_OPEN_TAG;
const END_MARKER: &'static str = SKILLS_INSTRUCTIONS_CLOSE_TAG;
fn body(&self) -> String {
let mut lines: Vec<String> = Vec::new();
lines.push("## Skills".to_string());
lines.push("A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.".to_string());
lines.push("### Available skills".to_string());
lines.extend(self.skill_lines.iter().cloned());
lines.push("### How to use skills".to_string());
lines.push(
r###"- Discovery: The list above is the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.
- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
- How to use a skill (progressive disclosure):
1) After deciding to use a skill, open its `SKILL.md`. Read only enough to follow the workflow.
2) When `SKILL.md` references relative paths (e.g., `scripts/foo.py`), resolve them relative to the skill directory listed above first, and only consider other paths if needed.
3) If `SKILL.md` points to extra folders such as `references/`, load only the specific files needed for the request; don't bulk-load everything.
4) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.
5) If `assets/` or templates exist, reuse them instead of recreating from scratch.
- Coordination and sequencing:
- If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
- Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
- Context hygiene:
- Keep context small: summarize long sections instead of pasting them; only load extra files when needed.
- Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
- When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue."###
.to_string(),
);
format!("\n{}\n", lines.join("\n"))
}
}
@@ -0,0 +1,32 @@
use super::ContextualUserFragment;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::protocol::COLLABORATION_MODE_CLOSE_TAG;
use codex_protocol::protocol::COLLABORATION_MODE_OPEN_TAG;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct CollaborationModeInstructions {
instructions: String,
}
impl CollaborationModeInstructions {
pub(crate) fn from_collaboration_mode(collaboration_mode: &CollaborationMode) -> Option<Self> {
collaboration_mode
.settings
.developer_instructions
.as_ref()
.filter(|instructions| !instructions.is_empty())
.map(|instructions| Self {
instructions: instructions.clone(),
})
}
}
impl ContextualUserFragment for CollaborationModeInstructions {
const ROLE: &'static str = "developer";
const START_MARKER: &'static str = COLLABORATION_MODE_OPEN_TAG;
const END_MARKER: &'static str = COLLABORATION_MODE_CLOSE_TAG;
fn body(&self) -> String {
self.instructions.clone()
}
}
@@ -200,7 +200,7 @@ impl ContextualUserFragment for EnvironmentContext {
lines.extend(subagents.lines().map(|line| format!(" {line}")));
lines.push(" </subagents>".to_string());
}
format!("\n{}", lines.join("\n"))
format!("\n{}\n", lines.join("\n"))
}
}
+18 -12
View File
@@ -28,13 +28,16 @@ impl<T: ContextualUserFragment> FragmentRegistration for FragmentRegistrationPro
}
}
/// Context payload that is injected as a user-authored message fragment.
/// Context payload that is injected as a message fragment.
///
/// Implementations own the response role, start/end markers used to recognize
/// the fragment, and provide the fragment body appended directly after the
/// start marker. The default helpers wrap that body and convert it into the
/// response item shape expected by model input assembly.
pub(crate) trait ContextualUserFragment {
/// Implementations own the response role and provide the exact fragment body.
/// Marked fragments also provide start/end markers used to recognize injected
/// context later. `render()` concatenates markers and body without adding
/// separators, so implementations should include any whitespace they need
/// between tags in `body()`. Unmarked fragments should leave both markers empty,
/// in which case the default helpers render only the body and never match
/// arbitrary text.
pub trait ContextualUserFragment {
const ROLE: &'static str;
const START_MARKER: &'static str;
const END_MARKER: &'static str;
@@ -45,6 +48,10 @@ pub(crate) trait ContextualUserFragment {
where
Self: Sized,
{
if Self::START_MARKER.is_empty() || Self::END_MARKER.is_empty() {
return false;
}
let trimmed = text.trim_start();
let starts_with_marker = trimmed
.get(..Self::START_MARKER.len())
@@ -57,12 +64,11 @@ pub(crate) trait ContextualUserFragment {
}
fn render(&self) -> String {
format!(
"{}{}\n{}",
Self::START_MARKER,
self.body(),
Self::END_MARKER
)
if Self::START_MARKER.is_empty() && Self::END_MARKER.is_empty() {
return self.body();
}
format!("{}{}{}", Self::START_MARKER, self.body(), Self::END_MARKER)
}
fn into(self) -> ResponseItem
@@ -0,0 +1,21 @@
use super::ContextualUserFragment;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct GuardianFollowupReviewReminder;
impl ContextualUserFragment for GuardianFollowupReviewReminder {
const ROLE: &'static str = "developer";
const START_MARKER: &'static str = "";
const END_MARKER: &'static str = "";
fn body(&self) -> String {
concat!(
"Use prior reviews as context, not binding precedent. ",
"Follow the Workspace Policy. ",
"If the user explicitly approves a previously rejected action after being informed of the ",
"concrete risks, set outcome to \"allow\" unless the policy explicitly disallows user ",
"overwrites in such cases."
)
.to_string()
}
}
@@ -0,0 +1,22 @@
use super::ContextualUserFragment;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct HookAdditionalContext {
text: String,
}
impl HookAdditionalContext {
pub(crate) fn new(text: impl Into<String>) -> Self {
Self { text: text.into() }
}
}
impl ContextualUserFragment for HookAdditionalContext {
const ROLE: &'static str = "developer";
const START_MARKER: &'static str = "";
const END_MARKER: &'static str = "";
fn body(&self) -> String {
self.text.clone()
}
}
@@ -0,0 +1,30 @@
use super::ContextualUserFragment;
use std::fmt::Display;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ImageGenerationInstructions {
image_output_dir: String,
image_output_path: String,
}
impl ImageGenerationInstructions {
pub(crate) fn new(image_output_dir: impl Display, image_output_path: impl Display) -> Self {
Self {
image_output_dir: image_output_dir.to_string(),
image_output_path: image_output_path.to_string(),
}
}
}
impl ContextualUserFragment for ImageGenerationInstructions {
const ROLE: &'static str = "developer";
const START_MARKER: &'static str = "";
const END_MARKER: &'static str = "";
fn body(&self) -> String {
format!(
"Generated images are saved to {} as {} by default.\nIf you need to use a generated image at another path, copy it and leave the original in place unless the user explicitly asks you to delete it.",
self.image_output_dir, self.image_output_path
)
}
}
+37 -1
View File
@@ -1,20 +1,56 @@
//! Context fragments injected into model input.
mod approved_command_prefix_saved;
mod apps_instructions;
mod available_plugins_instructions;
mod available_skills_instructions;
mod collaboration_mode_instructions;
mod contextual_user_message;
mod environment_context;
mod fragment;
mod guardian_followup_review_reminder;
mod hook_additional_context;
mod image_generation_instructions;
mod model_switch_instructions;
mod network_rule_saved;
mod permissions_instructions;
mod personality_spec_instructions;
mod plugin_instructions;
mod realtime_end_instructions;
mod realtime_start_instructions;
mod realtime_start_with_instructions;
mod skill_instructions;
mod spawn_agent_instructions;
mod subagent_notification;
mod turn_aborted;
mod user_instructions;
mod user_shell_command;
pub(crate) use approved_command_prefix_saved::ApprovedCommandPrefixSaved;
pub(crate) use apps_instructions::AppsInstructions;
pub(crate) use available_plugins_instructions::AvailablePluginsInstructions;
pub(crate) use available_skills_instructions::AvailableSkillsInstructions;
pub(crate) use collaboration_mode_instructions::CollaborationModeInstructions;
pub(crate) use contextual_user_message::is_contextual_user_fragment;
pub(crate) use contextual_user_message::is_memory_excluded_contextual_user_fragment;
pub(crate) use contextual_user_message::parse_visible_hook_prompt_message;
pub(crate) use environment_context::EnvironmentContext;
pub(crate) use fragment::ContextualUserFragment;
pub use fragment::ContextualUserFragment;
pub(crate) use fragment::FragmentRegistration;
pub(crate) use fragment::FragmentRegistrationProxy;
pub(crate) use guardian_followup_review_reminder::GuardianFollowupReviewReminder;
pub(crate) use hook_additional_context::HookAdditionalContext;
pub(crate) use image_generation_instructions::ImageGenerationInstructions;
pub(crate) use model_switch_instructions::ModelSwitchInstructions;
pub(crate) use network_rule_saved::NetworkRuleSaved;
pub use permissions_instructions::PermissionsInstructions;
pub(crate) use personality_spec_instructions::PersonalitySpecInstructions;
pub(crate) use plugin_instructions::PluginInstructions;
pub(crate) use realtime_end_instructions::RealtimeEndInstructions;
pub(crate) use realtime_start_instructions::RealtimeStartInstructions;
pub(crate) use realtime_start_with_instructions::RealtimeStartWithInstructions;
pub(crate) use skill_instructions::SkillInstructions;
pub(crate) use spawn_agent_instructions::SpawnAgentInstructions;
pub(crate) use subagent_notification::SubagentNotification;
pub(crate) use turn_aborted::TurnAborted;
pub(crate) use user_instructions::UserInstructions;
@@ -0,0 +1,27 @@
use super::ContextualUserFragment;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ModelSwitchInstructions {
model_instructions: String,
}
impl ModelSwitchInstructions {
pub(crate) fn new(model_instructions: impl Into<String>) -> Self {
Self {
model_instructions: model_instructions.into(),
}
}
}
impl ContextualUserFragment for ModelSwitchInstructions {
const ROLE: &'static str = "developer";
const START_MARKER: &'static str = "<model_switch>";
const END_MARKER: &'static str = "</model_switch>";
fn body(&self) -> String {
format!(
"\nThe user was previously using a different model. Please continue the conversation according to the following instructions:\n\n{}\n",
self.model_instructions
)
}
}
@@ -0,0 +1,35 @@
use super::ContextualUserFragment;
use codex_protocol::approvals::NetworkPolicyAmendment;
use codex_protocol::approvals::NetworkPolicyRuleAction;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct NetworkRuleSaved {
action: NetworkPolicyRuleAction,
host: String,
}
impl NetworkRuleSaved {
pub(crate) fn new(amendment: &NetworkPolicyAmendment) -> Self {
Self {
action: amendment.action,
host: amendment.host.clone(),
}
}
}
impl ContextualUserFragment for NetworkRuleSaved {
const ROLE: &'static str = "developer";
const START_MARKER: &'static str = "";
const END_MARKER: &'static str = "";
fn body(&self) -> String {
let (action, list_name) = match self.action {
NetworkPolicyRuleAction::Allow => ("Allowed", "allowlist"),
NetworkPolicyRuleAction::Deny => ("Denied", "denylist"),
};
format!(
"{action} network rule saved in execpolicy ({list_name}): {}",
self.host
)
}
}
@@ -0,0 +1,316 @@
use super::ContextualUserFragment;
use codex_execpolicy::Policy;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::models::format_allow_prefixes;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::GranularApprovalConfig;
use codex_protocol::protocol::NetworkAccess;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::WritableRoot;
use codex_utils_template::Template;
use std::path::Path;
use std::sync::LazyLock;
const APPROVAL_POLICY_NEVER: &str = include_str!("prompts/permissions/approval_policy/never.md");
const APPROVAL_POLICY_UNLESS_TRUSTED: &str =
include_str!("prompts/permissions/approval_policy/unless_trusted.md");
const APPROVAL_POLICY_ON_FAILURE: &str =
include_str!("prompts/permissions/approval_policy/on_failure.md");
const APPROVAL_POLICY_ON_REQUEST_RULE: &str =
include_str!("prompts/permissions/approval_policy/on_request.md");
const APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION: &str =
include_str!("prompts/permissions/approval_policy/on_request_rule_request_permission.md");
const AUTO_REVIEW_APPROVAL_SUFFIX: &str = "`approvals_reviewer` is `auto_review`: Sandbox escalations with require_escalated will be reviewed for compliance with the policy. If a rejection happens, you should proceed only with a materially safer alternative, or inform the user of the risk and send a final message to ask for approval.";
const SANDBOX_MODE_DANGER_FULL_ACCESS: &str =
include_str!("prompts/permissions/sandbox_mode/danger_full_access.md");
const SANDBOX_MODE_WORKSPACE_WRITE: &str =
include_str!("prompts/permissions/sandbox_mode/workspace_write.md");
const SANDBOX_MODE_READ_ONLY: &str = include_str!("prompts/permissions/sandbox_mode/read_only.md");
static SANDBOX_MODE_DANGER_FULL_ACCESS_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
Template::parse(SANDBOX_MODE_DANGER_FULL_ACCESS.trim_end())
.unwrap_or_else(|err| panic!("danger-full-access sandbox template must parse: {err}"))
});
static SANDBOX_MODE_WORKSPACE_WRITE_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
Template::parse(SANDBOX_MODE_WORKSPACE_WRITE.trim_end())
.unwrap_or_else(|err| panic!("workspace-write sandbox template must parse: {err}"))
});
static SANDBOX_MODE_READ_ONLY_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
Template::parse(SANDBOX_MODE_READ_ONLY.trim_end())
.unwrap_or_else(|err| panic!("read-only sandbox template must parse: {err}"))
});
struct PermissionsPromptConfig<'a> {
approval_policy: AskForApproval,
approvals_reviewer: ApprovalsReviewer,
exec_policy: &'a Policy,
exec_permission_approvals_enabled: bool,
request_permissions_tool_enabled: bool,
}
#[derive(Debug, Clone, PartialEq)]
/// Developer instructions that describe the active sandbox and approval policy.
pub struct PermissionsInstructions {
text: String,
}
impl PermissionsInstructions {
/// Builds permissions instructions from the effective sandbox and approval policy.
pub fn from_policy(
sandbox_policy: &SandboxPolicy,
approval_policy: AskForApproval,
approvals_reviewer: ApprovalsReviewer,
exec_policy: &Policy,
cwd: &Path,
exec_permission_approvals_enabled: bool,
request_permissions_tool_enabled: bool,
) -> Self {
let network_access = if sandbox_policy.has_full_network_access() {
NetworkAccess::Enabled
} else {
NetworkAccess::Restricted
};
let (sandbox_mode, writable_roots) = match sandbox_policy {
SandboxPolicy::DangerFullAccess => (SandboxMode::DangerFullAccess, None),
SandboxPolicy::ReadOnly { .. } => (SandboxMode::ReadOnly, None),
SandboxPolicy::ExternalSandbox { .. } => (SandboxMode::DangerFullAccess, None),
SandboxPolicy::WorkspaceWrite { .. } => {
let roots = sandbox_policy.get_writable_roots_with_cwd(cwd);
(SandboxMode::WorkspaceWrite, Some(roots))
}
};
Self::from_permissions_with_network(
sandbox_mode,
network_access,
PermissionsPromptConfig {
approval_policy,
approvals_reviewer,
exec_policy,
exec_permission_approvals_enabled,
request_permissions_tool_enabled,
},
writable_roots,
)
}
fn from_permissions_with_network(
sandbox_mode: SandboxMode,
network_access: NetworkAccess,
config: PermissionsPromptConfig<'_>,
writable_roots: Option<Vec<WritableRoot>>,
) -> Self {
let mut text = String::new();
append_section(&mut text, &sandbox_text(sandbox_mode, network_access));
append_section(
&mut text,
&approval_text(
config.approval_policy,
config.approvals_reviewer,
config.exec_policy,
config.exec_permission_approvals_enabled,
config.request_permissions_tool_enabled,
),
);
if let Some(writable_roots) = writable_roots_text(writable_roots) {
append_section(&mut text, &writable_roots);
}
if !text.ends_with('\n') {
text.push('\n');
}
Self { text }
}
}
impl ContextualUserFragment for PermissionsInstructions {
const ROLE: &'static str = "developer";
const START_MARKER: &'static str = "<permissions instructions>";
const END_MARKER: &'static str = "</permissions instructions>";
fn body(&self) -> String {
self.text.clone()
}
}
fn append_section(text: &mut String, section: &str) {
if !text.ends_with('\n') {
text.push('\n');
}
text.push_str(section);
}
fn approval_text(
approval_policy: AskForApproval,
approvals_reviewer: ApprovalsReviewer,
exec_policy: &Policy,
exec_permission_approvals_enabled: bool,
request_permissions_tool_enabled: bool,
) -> String {
let with_request_permissions_tool = |text: &str| {
if request_permissions_tool_enabled {
format!("{text}\n\n{}", request_permissions_tool_prompt_section())
} else {
text.to_string()
}
};
let on_request_instructions = || {
let on_request_rule = if exec_permission_approvals_enabled {
APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION.to_string()
} else {
APPROVAL_POLICY_ON_REQUEST_RULE.to_string()
};
let mut sections = vec![on_request_rule];
if request_permissions_tool_enabled {
sections.push(request_permissions_tool_prompt_section().to_string());
}
if let Some(prefixes) = approved_command_prefixes_text(exec_policy) {
sections.push(format!(
"## Approved command prefixes\nThe following prefix rules have already been approved: {prefixes}"
));
}
sections.join("\n\n")
};
let text = match approval_policy {
AskForApproval::Never => APPROVAL_POLICY_NEVER.to_string(),
AskForApproval::UnlessTrusted => {
with_request_permissions_tool(APPROVAL_POLICY_UNLESS_TRUSTED)
}
AskForApproval::OnFailure => with_request_permissions_tool(APPROVAL_POLICY_ON_FAILURE),
AskForApproval::OnRequest => on_request_instructions(),
AskForApproval::Granular(granular_config) => granular_instructions(
granular_config,
exec_policy,
exec_permission_approvals_enabled,
request_permissions_tool_enabled,
),
};
if approvals_reviewer == ApprovalsReviewer::GuardianSubagent
&& approval_policy != AskForApproval::Never
{
format!("{text}\n\n{AUTO_REVIEW_APPROVAL_SUFFIX}")
} else {
text
}
}
fn sandbox_text(mode: SandboxMode, network_access: NetworkAccess) -> String {
let template = match mode {
SandboxMode::DangerFullAccess => &*SANDBOX_MODE_DANGER_FULL_ACCESS_TEMPLATE,
SandboxMode::WorkspaceWrite => &*SANDBOX_MODE_WORKSPACE_WRITE_TEMPLATE,
SandboxMode::ReadOnly => &*SANDBOX_MODE_READ_ONLY_TEMPLATE,
};
let network_access = network_access.to_string();
template
.render([("network_access", network_access.as_str())])
.unwrap_or_else(|err| panic!("sandbox template must render: {err}"))
}
fn writable_roots_text(writable_roots: Option<Vec<WritableRoot>>) -> Option<String> {
let roots = writable_roots?;
if roots.is_empty() {
return None;
}
let roots_list: Vec<String> = roots
.iter()
.map(|r| format!("`{}`", r.root.to_string_lossy()))
.collect();
Some(if roots_list.len() == 1 {
format!(" The writable root is {}.", roots_list[0])
} else {
format!(" The writable roots are {}.", roots_list.join(", "))
})
}
fn approved_command_prefixes_text(exec_policy: &Policy) -> Option<String> {
format_allow_prefixes(exec_policy.get_allowed_prefixes())
.filter(|prefixes| !prefixes.is_empty())
}
fn granular_prompt_intro_text() -> &'static str {
"# Approval Requests\n\nApproval policy is `granular`. Categories set to `false` are automatically rejected instead of prompting the user."
}
fn request_permissions_tool_prompt_section() -> &'static str {
"# request_permissions Tool\n\nThe built-in `request_permissions` tool is available in this session. Invoke it when you need to request additional `network` or `file_system` permissions before later shell-like commands need them. Request only the specific permissions required for the task."
}
fn granular_instructions(
granular_config: GranularApprovalConfig,
exec_policy: &Policy,
exec_permission_approvals_enabled: bool,
request_permissions_tool_enabled: bool,
) -> String {
let sandbox_approval_prompts_allowed = granular_config.allows_sandbox_approval();
let shell_permission_requests_available =
exec_permission_approvals_enabled && sandbox_approval_prompts_allowed;
let request_permissions_tool_prompts_allowed =
request_permissions_tool_enabled && granular_config.allows_request_permissions();
let categories = [
Some((
granular_config.allows_sandbox_approval(),
"`sandbox_approval`",
)),
Some((granular_config.allows_rules_approval(), "`rules`")),
Some((granular_config.allows_skill_approval(), "`skill_approval`")),
request_permissions_tool_enabled.then_some((
granular_config.allows_request_permissions(),
"`request_permissions`",
)),
Some((
granular_config.allows_mcp_elicitations(),
"`mcp_elicitations`",
)),
];
let prompted_categories = categories
.iter()
.flatten()
.filter(|&&(is_allowed, _)| is_allowed)
.map(|&(_, category)| format!("- {category}"))
.collect::<Vec<_>>();
let rejected_categories = categories
.iter()
.flatten()
.filter(|&&(is_allowed, _)| !is_allowed)
.map(|&(_, category)| format!("- {category}"))
.collect::<Vec<_>>();
let mut sections = vec![granular_prompt_intro_text().to_string()];
if !prompted_categories.is_empty() {
sections.push(format!(
"These approval categories may still prompt the user when needed:\n{}",
prompted_categories.join("\n")
));
}
if !rejected_categories.is_empty() {
sections.push(format!(
"These approval categories are automatically rejected instead of prompting the user:\n{}",
rejected_categories.join("\n")
));
}
if shell_permission_requests_available {
sections.push(APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION.to_string());
}
if request_permissions_tool_prompts_allowed {
sections.push(request_permissions_tool_prompt_section().to_string());
}
if let Some(prefixes) = approved_command_prefixes_text(exec_policy) {
sections.push(format!(
"## Approved command prefixes\nThe following prefix rules have already been approved: {prefixes}"
));
}
sections.join("\n\n")
}
#[cfg(test)]
#[path = "permissions_instructions_tests.rs"]
mod permissions_instructions_tests;
@@ -0,0 +1,412 @@
use super::*;
use codex_execpolicy::Decision;
use pretty_assertions::assert_eq;
use std::path::PathBuf;
#[test]
fn renders_sandbox_mode_text() {
assert_eq!(
sandbox_text(SandboxMode::WorkspaceWrite, NetworkAccess::Restricted),
"Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: The sandbox permits reading files, and editing files in `cwd` and `writable_roots`. Editing files in other directories requires approval. Network access is restricted."
);
assert_eq!(
sandbox_text(SandboxMode::ReadOnly, NetworkAccess::Restricted),
"Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `read-only`: The sandbox only permits reading files. Network access is restricted."
);
assert_eq!(
sandbox_text(SandboxMode::DangerFullAccess, NetworkAccess::Enabled),
"Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`: No filesystem sandboxing - all commands are permitted. Network access is enabled."
);
}
#[test]
fn builds_permissions_with_network_access_override() {
let instructions = PermissionsInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Enabled,
PermissionsPromptConfig {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: false,
request_permissions_tool_enabled: false,
},
/*writable_roots*/ None,
);
let text = instructions.body();
assert!(
text.contains("Network access is enabled."),
"expected network access to be enabled in message"
);
assert!(
text.contains("How to request escalation"),
"expected approval guidance to be included"
);
}
#[test]
fn builds_permissions_from_policy() {
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
read_only_access: Default::default(),
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};
let instructions = PermissionsInstructions::from_policy(
&policy,
AskForApproval::UnlessTrusted,
ApprovalsReviewer::User,
&Policy::empty(),
&PathBuf::from("/tmp"),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
);
let text = instructions.body();
assert!(text.contains("Network access is enabled."));
assert!(text.contains("`approval_policy` is `unless-trusted`"));
}
#[test]
fn includes_request_rule_instructions_for_on_request() {
let mut exec_policy = Policy::empty();
exec_policy
.add_prefix_rule(&["git".to_string(), "pull".to_string()], Decision::Allow)
.expect("add rule");
let instructions = PermissionsInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Enabled,
PermissionsPromptConfig {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
exec_policy: &exec_policy,
exec_permission_approvals_enabled: false,
request_permissions_tool_enabled: false,
},
/*writable_roots*/ None,
);
let text = instructions.body();
assert!(text.contains("prefix_rule"));
assert!(text.contains("Approved command prefixes"));
assert!(text.contains(r#"["git", "pull"]"#));
}
#[test]
fn includes_request_permissions_tool_instructions_for_unless_trusted_when_enabled() {
let instructions = PermissionsInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Enabled,
PermissionsPromptConfig {
approval_policy: AskForApproval::UnlessTrusted,
approvals_reviewer: ApprovalsReviewer::User,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: false,
request_permissions_tool_enabled: true,
},
/*writable_roots*/ None,
);
let text = instructions.body();
assert!(text.contains("`approval_policy` is `unless-trusted`"));
assert!(text.contains("# request_permissions Tool"));
}
#[test]
fn includes_request_permissions_tool_instructions_for_on_failure_when_enabled() {
let instructions = PermissionsInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Enabled,
PermissionsPromptConfig {
approval_policy: AskForApproval::OnFailure,
approvals_reviewer: ApprovalsReviewer::User,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: false,
request_permissions_tool_enabled: true,
},
/*writable_roots*/ None,
);
let text = instructions.body();
assert!(text.contains("`approval_policy` is `on-failure`"));
assert!(text.contains("# request_permissions Tool"));
}
#[test]
fn includes_request_permission_rule_instructions_for_on_request_when_enabled() {
let instructions = PermissionsInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Enabled,
PermissionsPromptConfig {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: true,
request_permissions_tool_enabled: false,
},
/*writable_roots*/ None,
);
let text = instructions.body();
assert!(text.contains("with_additional_permissions"));
assert!(text.contains("additional_permissions"));
}
#[test]
fn includes_request_permissions_tool_instructions_for_on_request_when_tool_is_enabled() {
let instructions = PermissionsInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Enabled,
PermissionsPromptConfig {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: false,
request_permissions_tool_enabled: true,
},
/*writable_roots*/ None,
);
let text = instructions.body();
assert!(text.contains("# request_permissions Tool"));
assert!(text.contains("The built-in `request_permissions` tool is available in this session."));
}
#[test]
fn on_request_includes_tool_guidance_alongside_inline_permission_guidance_when_both_exist() {
let instructions = PermissionsInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Enabled,
PermissionsPromptConfig {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: true,
request_permissions_tool_enabled: true,
},
/*writable_roots*/ None,
);
let text = instructions.body();
assert!(text.contains("with_additional_permissions"));
assert!(text.contains("# request_permissions Tool"));
}
#[test]
fn guardian_subagent_approvals_append_guardian_specific_guidance() {
let text = approval_text(
AskForApproval::OnRequest,
ApprovalsReviewer::GuardianSubagent,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
);
assert!(text.contains("`approvals_reviewer` is `auto_review`"));
assert!(!text.contains("`approvals_reviewer` is `guardian_subagent`"));
assert!(text.contains("materially safer alternative"));
}
#[test]
fn guardian_subagent_approvals_omit_guardian_specific_guidance_when_approval_is_never() {
let text = approval_text(
AskForApproval::Never,
ApprovalsReviewer::GuardianSubagent,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
);
assert!(!text.contains("`approvals_reviewer` is `auto_review`"));
assert!(!text.contains("`approvals_reviewer` is `guardian_subagent`"));
}
fn granular_categories_section(title: &str, categories: &[&str]) -> String {
format!("{title}\n{}", categories.join("\n"))
}
fn granular_prompt_expected(
prompted_categories: &[&str],
rejected_categories: &[&str],
include_shell_permission_request_instructions: bool,
include_request_permissions_tool_section: bool,
) -> String {
let mut sections = vec![granular_prompt_intro_text().to_string()];
if !prompted_categories.is_empty() {
sections.push(granular_categories_section(
"These approval categories may still prompt the user when needed:",
prompted_categories,
));
}
if !rejected_categories.is_empty() {
sections.push(granular_categories_section(
"These approval categories are automatically rejected instead of prompting the user:",
rejected_categories,
));
}
if include_shell_permission_request_instructions {
sections.push(APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION.to_string());
}
if include_request_permissions_tool_section {
sections.push(request_permissions_tool_prompt_section().to_string());
}
sections.join("\n\n")
}
#[test]
fn granular_policy_lists_prompted_and_rejected_categories_separately() {
let text = approval_text(
AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: false,
rules: true,
skill_approval: false,
request_permissions: true,
mcp_elicitations: false,
}),
ApprovalsReviewer::User,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ false,
);
assert_eq!(
text,
[
granular_prompt_intro_text().to_string(),
granular_categories_section(
"These approval categories may still prompt the user when needed:",
&["- `rules`"],
),
granular_categories_section(
"These approval categories are automatically rejected instead of prompting the user:",
&[
"- `sandbox_approval`",
"- `skill_approval`",
"- `mcp_elicitations`",
],
),
]
.join("\n\n")
);
}
#[test]
fn granular_policy_includes_command_permission_instructions_when_sandbox_approval_can_prompt() {
let text = approval_text(
AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: true,
rules: true,
skill_approval: true,
request_permissions: true,
mcp_elicitations: true,
}),
ApprovalsReviewer::User,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ false,
);
assert_eq!(
text,
granular_prompt_expected(
&[
"- `sandbox_approval`",
"- `rules`",
"- `skill_approval`",
"- `mcp_elicitations`",
],
&[],
/*include_shell_permission_request_instructions*/ true,
/*include_request_permissions_tool_section*/ false,
)
);
}
#[test]
fn granular_policy_omits_shell_permission_instructions_when_inline_requests_are_disabled() {
let text = approval_text(
AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: true,
rules: true,
skill_approval: true,
request_permissions: true,
mcp_elicitations: true,
}),
ApprovalsReviewer::User,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
);
assert_eq!(
text,
granular_prompt_expected(
&[
"- `sandbox_approval`",
"- `rules`",
"- `skill_approval`",
"- `mcp_elicitations`",
],
&[],
/*include_shell_permission_request_instructions*/ false,
/*include_request_permissions_tool_section*/ false,
)
);
}
#[test]
fn granular_policy_includes_request_permissions_tool_only_when_that_prompt_can_still_fire() {
let allowed = approval_text(
AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: true,
rules: true,
skill_approval: true,
request_permissions: true,
mcp_elicitations: true,
}),
ApprovalsReviewer::User,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ true,
);
assert!(allowed.contains("# request_permissions Tool"));
let rejected = approval_text(
AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: true,
rules: true,
skill_approval: true,
request_permissions: false,
mcp_elicitations: true,
}),
ApprovalsReviewer::User,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ true,
);
assert!(!rejected.contains("# request_permissions Tool"));
}
#[test]
fn granular_policy_lists_request_permissions_category_without_tool_section_when_tool_unavailable() {
let text = approval_text(
AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: false,
rules: false,
skill_approval: false,
request_permissions: true,
mcp_elicitations: false,
}),
ApprovalsReviewer::User,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ false,
);
assert!(!text.contains("- `request_permissions`"));
assert!(!text.contains("# request_permissions Tool"));
}
@@ -0,0 +1,25 @@
use super::ContextualUserFragment;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct PersonalitySpecInstructions {
spec: String,
}
impl PersonalitySpecInstructions {
pub(crate) fn new(spec: impl Into<String>) -> Self {
Self { spec: spec.into() }
}
}
impl ContextualUserFragment for PersonalitySpecInstructions {
const ROLE: &'static str = "developer";
const START_MARKER: &'static str = "<personality_spec>";
const END_MARKER: &'static str = "</personality_spec>";
fn body(&self) -> String {
format!(
" The user has requested a new communication style. Future messages should adhere to the following personality: \n{} ",
self.spec
)
}
}
@@ -0,0 +1,22 @@
use super::ContextualUserFragment;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct PluginInstructions {
text: String,
}
impl PluginInstructions {
pub(crate) fn new(text: impl Into<String>) -> Self {
Self { text: text.into() }
}
}
impl ContextualUserFragment for PluginInstructions {
const ROLE: &'static str = "developer";
const START_MARKER: &'static str = "";
const END_MARKER: &'static str = "";
fn body(&self) -> String {
self.text.clone()
}
}
@@ -0,0 +1,32 @@
use super::ContextualUserFragment;
use codex_protocol::protocol::REALTIME_CONVERSATION_CLOSE_TAG;
use codex_protocol::protocol::REALTIME_CONVERSATION_OPEN_TAG;
const REALTIME_END_INSTRUCTIONS: &str = include_str!("prompts/realtime/realtime_end.md");
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RealtimeEndInstructions {
reason: String,
}
impl RealtimeEndInstructions {
pub(crate) fn new(reason: impl Into<String>) -> Self {
Self {
reason: reason.into(),
}
}
}
impl ContextualUserFragment for RealtimeEndInstructions {
const ROLE: &'static str = "developer";
const START_MARKER: &'static str = REALTIME_CONVERSATION_OPEN_TAG;
const END_MARKER: &'static str = REALTIME_CONVERSATION_CLOSE_TAG;
fn body(&self) -> String {
format!(
"\n{}\n\nReason: {}\n",
REALTIME_END_INSTRUCTIONS.trim(),
self.reason
)
}
}
@@ -0,0 +1,18 @@
use super::ContextualUserFragment;
use codex_protocol::protocol::REALTIME_CONVERSATION_CLOSE_TAG;
use codex_protocol::protocol::REALTIME_CONVERSATION_OPEN_TAG;
const REALTIME_START_INSTRUCTIONS: &str = include_str!("prompts/realtime/realtime_start.md");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct RealtimeStartInstructions;
impl ContextualUserFragment for RealtimeStartInstructions {
const ROLE: &'static str = "developer";
const START_MARKER: &'static str = REALTIME_CONVERSATION_OPEN_TAG;
const END_MARKER: &'static str = REALTIME_CONVERSATION_CLOSE_TAG;
fn body(&self) -> String {
format!("\n{}\n", REALTIME_START_INSTRUCTIONS.trim())
}
}
@@ -0,0 +1,26 @@
use super::ContextualUserFragment;
use codex_protocol::protocol::REALTIME_CONVERSATION_CLOSE_TAG;
use codex_protocol::protocol::REALTIME_CONVERSATION_OPEN_TAG;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RealtimeStartWithInstructions {
instructions: String,
}
impl RealtimeStartWithInstructions {
pub(crate) fn new(instructions: impl Into<String>) -> Self {
Self {
instructions: instructions.into(),
}
}
}
impl ContextualUserFragment for RealtimeStartWithInstructions {
const ROLE: &'static str = "developer";
const START_MARKER: &'static str = REALTIME_CONVERSATION_OPEN_TAG;
const END_MARKER: &'static str = REALTIME_CONVERSATION_CLOSE_TAG;
fn body(&self) -> String {
format!("\n{}\n", self.instructions)
}
}
@@ -26,7 +26,7 @@ impl ContextualUserFragment for SkillInstructions {
fn body(&self) -> String {
format!(
"\n<name>{}</name>\n<path>{}</path>\n{}",
"\n<name>{}</name>\n<path>{}</path>\n{}\n",
self.name, self.path, self.contents
)
}
@@ -0,0 +1,14 @@
use super::ContextualUserFragment;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct SpawnAgentInstructions;
impl ContextualUserFragment for SpawnAgentInstructions {
const ROLE: &'static str = "developer";
const START_MARKER: &'static str = "<spawned_agent_context>";
const END_MARKER: &'static str = "</spawned_agent_context>";
fn body(&self) -> String {
"\nYou are a newly spawned agent in a team of agents collaborating to complete a task. You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents. You are responsible for returning the response to your assigned task in the final channel. When you give your response, the contents of your response in the final channel will be immediately delivered back to your parent agent. The prior conversation history was forked from your parent agent. Treat the next user message as your assigned task, and use the forked history only as background context.\n".to_string()
}
}
@@ -24,7 +24,7 @@ impl ContextualUserFragment for SubagentNotification {
fn body(&self) -> String {
format!(
"\n{}",
"\n{}\n",
serde_json::json!({
"agent_path": &self.agent_reference,
"status": &self.status,
+1 -1
View File
@@ -21,6 +21,6 @@ impl ContextualUserFragment for TurnAborted {
const END_MARKER: &'static str = "</turn_aborted>";
fn body(&self) -> String {
format!("\n{}", self.guidance)
format!("\n{}\n", self.guidance)
}
}
@@ -12,6 +12,6 @@ impl ContextualUserFragment for UserInstructions {
const END_MARKER: &'static str = "</INSTRUCTIONS>";
fn body(&self) -> String {
format!("{}\n\n<INSTRUCTIONS>\n{}", self.directory, self.text)
format!("{}\n\n<INSTRUCTIONS>\n{}\n", self.directory, self.text)
}
}
@@ -33,7 +33,7 @@ impl ContextualUserFragment for UserShellCommand {
fn body(&self) -> String {
format!(
"\n<command>\n{}\n</command>\n<result>\nExit code: {}\nDuration: {:.4} seconds\nOutput:\n{}\n</result>",
"\n<command>\n{}\n</command>\n<result>\nExit code: {}\nDuration: {:.4} seconds\nOutput:\n{}\n</result>\n",
self.command, self.exit_code, self.duration_seconds, self.output,
)
}
+35 -28
View File
@@ -1,5 +1,12 @@
use crate::context::CollaborationModeInstructions;
use crate::context::ContextualUserFragment;
use crate::context::EnvironmentContext;
use crate::context::ModelSwitchInstructions;
use crate::context::PermissionsInstructions;
use crate::context::PersonalitySpecInstructions;
use crate::context::RealtimeEndInstructions;
use crate::context::RealtimeStartInstructions;
use crate::context::RealtimeStartWithInstructions;
use crate::session::PreviousTurnSettings;
use crate::session::turn_context::TurnContext;
use crate::shell::Shell;
@@ -7,7 +14,6 @@ use codex_execpolicy::Policy;
use codex_features::Feature;
use codex_protocol::config_types::Personality;
use codex_protocol::models::ContentItem;
use codex_protocol::models::DeveloperInstructions;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::protocol::TurnContextItem;
@@ -37,7 +43,7 @@ fn build_permissions_update_item(
previous: Option<&TurnContextItem>,
next: &TurnContext,
exec_policy: &Policy,
) -> Option<DeveloperInstructions> {
) -> Option<String> {
if !next.config.include_permissions_instructions {
return None;
}
@@ -49,28 +55,32 @@ fn build_permissions_update_item(
return None;
}
Some(DeveloperInstructions::from_policy(
next.sandbox_policy.get(),
next.approval_policy.value(),
next.config.approvals_reviewer,
exec_policy,
&next.cwd,
next.features.enabled(Feature::ExecPermissionApprovals),
next.features.enabled(Feature::RequestPermissionsTool),
))
Some(
PermissionsInstructions::from_policy(
next.sandbox_policy.get(),
next.approval_policy.value(),
next.config.approvals_reviewer,
exec_policy,
&next.cwd,
next.features.enabled(Feature::ExecPermissionApprovals),
next.features.enabled(Feature::RequestPermissionsTool),
)
.render(),
)
}
fn build_collaboration_mode_update_item(
previous: Option<&TurnContextItem>,
next: &TurnContext,
) -> Option<DeveloperInstructions> {
) -> Option<String> {
let prev = previous?;
if prev.collaboration_mode.as_ref() != Some(&next.collaboration_mode) {
// If the next mode has empty developer instructions, this returns None and we emit no
// update, so prior collaboration instructions remain in the prompt history.
Some(DeveloperInstructions::from_collaboration_mode(
&next.collaboration_mode,
)?)
Some(
CollaborationModeInstructions::from_collaboration_mode(&next.collaboration_mode)?
.render(),
)
} else {
None
}
@@ -80,28 +90,28 @@ pub(crate) fn build_realtime_update_item(
previous: Option<&TurnContextItem>,
previous_turn_settings: Option<&PreviousTurnSettings>,
next: &TurnContext,
) -> Option<DeveloperInstructions> {
) -> Option<String> {
match (
previous.and_then(|item| item.realtime_active),
next.realtime_active,
) {
(Some(true), false) => Some(DeveloperInstructions::realtime_end_message("inactive")),
(Some(true), false) => Some(RealtimeEndInstructions::new("inactive").render()),
(Some(false), true) | (None, true) => Some(
if let Some(instructions) = next
.config
.experimental_realtime_start_instructions
.as_deref()
{
DeveloperInstructions::realtime_start_message_with_instructions(instructions)
RealtimeStartWithInstructions::new(instructions).render()
} else {
DeveloperInstructions::realtime_start_message()
RealtimeStartInstructions.render()
},
),
(Some(true), true) | (Some(false), false) => None,
(None, false) => previous_turn_settings
.and_then(|settings| settings.realtime_active)
.filter(|realtime_active| *realtime_active)
.map(|_| DeveloperInstructions::realtime_end_message("inactive")),
.map(|_| RealtimeEndInstructions::new("inactive").render()),
}
}
@@ -109,7 +119,7 @@ pub(crate) fn build_initial_realtime_item(
previous: Option<&TurnContextItem>,
previous_turn_settings: Option<&PreviousTurnSettings>,
next: &TurnContext,
) -> Option<DeveloperInstructions> {
) -> Option<String> {
build_realtime_update_item(previous, previous_turn_settings, next)
}
@@ -117,7 +127,7 @@ fn build_personality_update_item(
previous: Option<&TurnContextItem>,
next: &TurnContext,
personality_feature_enabled: bool,
) -> Option<DeveloperInstructions> {
) -> Option<String> {
if !personality_feature_enabled {
return None;
}
@@ -131,7 +141,7 @@ fn build_personality_update_item(
{
let model_info = &next.model_info;
let personality_message = personality_message_for(model_info, personality);
personality_message.map(DeveloperInstructions::personality_spec_message)
personality_message.map(|message| PersonalitySpecInstructions::new(message).render())
} else {
None
}
@@ -151,7 +161,7 @@ pub(crate) fn personality_message_for(
pub(crate) fn build_model_instructions_update_item(
previous_turn_settings: Option<&PreviousTurnSettings>,
next: &TurnContext,
) -> Option<DeveloperInstructions> {
) -> Option<String> {
let previous_turn_settings = previous_turn_settings?;
if previous_turn_settings.model == next.model_info.slug {
return None;
@@ -162,9 +172,7 @@ pub(crate) fn build_model_instructions_update_item(
return None;
}
Some(DeveloperInstructions::model_switch_message(
model_instructions,
))
Some(ModelSwitchInstructions::new(model_instructions).render())
}
pub(crate) fn build_developer_update_item(text_sections: Vec<String>) -> Option<ResponseItem> {
@@ -218,7 +226,6 @@ pub(crate) fn build_settings_update_items(
]
.into_iter()
.flatten()
.map(DeveloperInstructions::into_text)
.collect();
let mut items = Vec::with_capacity(2);
+3 -11
View File
@@ -7,7 +7,6 @@ use std::time::Duration;
use anyhow::anyhow;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::models::DeveloperInstructions;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::protocol::AskForApproval;
@@ -29,6 +28,8 @@ use crate::config::Constrained;
use crate::config::ManagedFeatures;
use crate::config::NetworkProxySpec;
use crate::config::Permissions;
use crate::context::ContextualUserFragment;
use crate::context::GuardianFollowupReviewReminder;
use crate::rollout::recorder::RolloutRecorder;
use crate::session::Codex;
use crate::session::session::Session;
@@ -48,14 +49,6 @@ use super::prompt::guardian_policy_prompt;
use super::prompt::guardian_policy_prompt_with_config;
const GUARDIAN_INTERRUPT_DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
const GUARDIAN_FOLLOWUP_REVIEW_REMINDER: &str = concat!(
"Use prior reviews as context, not binding precedent. ",
"Follow the Workspace Policy. ",
"If the user explicitly approves a previously rejected action after being informed of the ",
"concrete risks, set outcome to \"allow\" unless the policy explicitly disallows user ",
"overwrites in such cases."
);
#[derive(Debug)]
pub(crate) enum GuardianReviewSessionOutcome {
Completed(anyhow::Result<Option<String>>),
@@ -634,8 +627,7 @@ async fn run_review_on_session(
async fn append_guardian_followup_reminder(review_session: &GuardianReviewSession) {
let turn_context = review_session.codex.session.new_default_turn().await;
let reminder: ResponseItem =
DeveloperInstructions::new(GUARDIAN_FOLLOWUP_REVIEW_REMINDER).into();
let reminder: ResponseItem = ContextualUserFragment::into(GuardianFollowupReviewReminder);
review_session
.codex
.session
+4 -2
View File
@@ -17,7 +17,6 @@ use codex_hooks::UserPromptSubmitRequest;
use codex_otel::HOOK_RUN_DURATION_METRIC;
use codex_otel::HOOK_RUN_METRIC;
use codex_protocol::items::TurnItem;
use codex_protocol::models::DeveloperInstructions;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::AskForApproval;
@@ -31,6 +30,8 @@ use codex_protocol::protocol::HookStartedEvent;
use codex_protocol::user_input::UserInput;
use serde_json::Value;
use crate::context::ContextualUserFragment;
use crate::context::HookAdditionalContext;
use crate::event_mapping::parse_turn_item;
use crate::session::session::Session;
use crate::session::turn_context::TurnContext;
@@ -340,7 +341,8 @@ pub(crate) async fn record_additional_contexts(
fn additional_context_messages(additional_contexts: Vec<String>) -> Vec<ResponseItem> {
additional_contexts
.into_iter()
.map(|additional_context| DeveloperInstructions::new(additional_context).into())
.map(HookAdditionalContext::new)
.map(ContextualUserFragment::into)
.collect()
}
+2 -2
View File
@@ -27,7 +27,7 @@ mod commit_attribution;
pub mod config;
pub mod config_loader;
pub mod connectors;
mod context;
pub mod context;
mod context_manager;
pub mod exec;
pub mod exec_env;
@@ -92,6 +92,7 @@ pub(crate) use skills::SkillLoadOutcome;
pub(crate) use skills::SkillMetadata;
pub(crate) use skills::SkillsLoadInput;
pub(crate) use skills::SkillsManager;
pub(crate) use skills::build_available_skills;
pub(crate) use skills::build_skill_injections;
pub(crate) use skills::build_skill_name_counts;
pub(crate) use skills::collect_env_var_dependencies;
@@ -100,7 +101,6 @@ pub(crate) use skills::default_skill_metadata_budget;
pub(crate) use skills::injection;
pub(crate) use skills::manager;
pub(crate) use skills::maybe_emit_implicit_skill_invocation;
pub(crate) use skills::render_skills_section;
pub(crate) use skills::resolve_skill_dependencies_for_turn;
pub(crate) use skills::skills_load_input_from_config;
mod skills_watcher;
+4 -3
View File
@@ -2,10 +2,11 @@ use std::collections::BTreeSet;
use std::collections::HashMap;
use codex_connectors::metadata::connector_display_label;
use codex_protocol::models::DeveloperInstructions;
use codex_protocol::models::ResponseItem;
use crate::connectors;
use crate::context::ContextualUserFragment;
use crate::context::PluginInstructions;
use crate::plugins::PluginCapabilitySummary;
use crate::plugins::render_explicit_plugin_instructions;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
@@ -52,8 +53,8 @@ pub(crate) fn build_plugin_injections(
.into_iter()
.collect::<Vec<_>>();
render_explicit_plugin_instructions(plugin, &available_mcp_servers, &available_apps)
.map(DeveloperInstructions::new)
.map(ResponseItem::from)
.map(PluginInstructions::new)
.map(ContextualUserFragment::into)
})
.collect()
}
-1
View File
@@ -55,7 +55,6 @@ pub use marketplace_remove::MarketplaceRemoveOutcome;
pub use marketplace_remove::MarketplaceRemoveRequest;
pub use marketplace_remove::remove_marketplace;
pub(crate) use render::render_explicit_plugin_instructions;
pub(crate) use render::render_plugins_section;
pub(crate) use startup_sync::curated_plugins_repo_path;
pub(crate) use startup_sync::read_curated_plugins_sha;
pub(crate) use startup_sync::sync_openai_plugins_repo;
+6 -36
View File
@@ -1,42 +1,12 @@
#[cfg(test)]
use crate::context::AvailablePluginsInstructions;
#[cfg(test)]
use crate::context::ContextualUserFragment;
use crate::plugins::PluginCapabilitySummary;
use codex_protocol::protocol::PLUGINS_INSTRUCTIONS_CLOSE_TAG;
use codex_protocol::protocol::PLUGINS_INSTRUCTIONS_OPEN_TAG;
#[cfg(test)]
pub(crate) fn render_plugins_section(plugins: &[PluginCapabilitySummary]) -> Option<String> {
if plugins.is_empty() {
return None;
}
let mut lines = vec![
"## Plugins".to_string(),
"A plugin is a local bundle of skills, MCP servers, and apps. Below is the list of plugins that are enabled and available in this session.".to_string(),
"### Available plugins".to_string(),
];
lines.extend(
plugins
.iter()
.map(|plugin| match plugin.description.as_deref() {
Some(description) => format!("- `{}`: {description}", plugin.display_name),
None => format!("- `{}`", plugin.display_name),
}),
);
lines.push("### How to use plugins".to_string());
lines.push(
r###"- Discovery: The list above is the plugins available in this session.
- Skill naming: If a plugin contributes skills, those skill entries are prefixed with `plugin_name:` in the Skills list.
- Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn.
- Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task.
- Preference: When a relevant plugin is available, prefer using capabilities associated with that plugin over standalone capabilities that provide similar functionality.
- Missing/blocked: If the user requests a plugin that is not listed above, or the plugin does not have relevant callable capabilities for the task, say so briefly and continue with the best fallback."###
.to_string(),
);
let body = lines.join("\n");
Some(format!(
"{PLUGINS_INSTRUCTIONS_OPEN_TAG}\n{body}\n{PLUGINS_INSTRUCTIONS_CLOSE_TAG}"
))
AvailablePluginsInstructions::from_plugins(plugins).map(|instructions| instructions.render())
}
pub(crate) fn render_explicit_plugin_instructions(
+36 -33
View File
@@ -14,19 +14,26 @@ use crate::agent::MailboxReceiver;
use crate::agent::agent_status_from_event;
use crate::agent::status::is_final;
use crate::agent_identity::AgentIdentityManager;
use crate::apps::render_apps_section;
use crate::build_available_skills;
use crate::commit_attribution::commit_message_trailer_instruction;
use crate::compact;
use crate::config::ManagedFeatures;
use crate::connectors;
use crate::context::ApprovedCommandPrefixSaved;
use crate::context::AppsInstructions;
use crate::context::AvailablePluginsInstructions;
use crate::context::AvailableSkillsInstructions;
use crate::context::CollaborationModeInstructions;
use crate::context::ContextualUserFragment;
use crate::context::NetworkRuleSaved;
use crate::context::PermissionsInstructions;
use crate::context::PersonalitySpecInstructions;
use crate::default_skill_metadata_budget;
use crate::exec_policy::ExecPolicyManager;
use crate::installation_id::resolve_installation_id;
use crate::parse_turn_item;
use crate::path_utils::normalize_for_native_workdir;
use crate::realtime_conversation::RealtimeConversationManager;
use crate::render_skills_section;
use crate::rollout::find_thread_name_by_id;
use crate::session_prefix::format_subagent_notification_message;
use crate::skills::SkillRenderSideEffects;
@@ -256,7 +263,6 @@ use crate::mcp::McpManager;
use crate::memories;
use crate::network_policy_decision::execpolicy_network_rule_amendment;
use crate::plugins::PluginsManager;
use crate::plugins::render_plugins_section;
use crate::rollout::RolloutRecorder;
use crate::rollout::RolloutRecorderParams;
use crate::rollout::map_session_init_error;
@@ -303,7 +309,6 @@ use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::ContentItem;
use codex_protocol::models::DeveloperInstructions;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
@@ -1652,8 +1657,9 @@ impl Session {
warn!("execpolicy amendment for {sub_id} had no command prefix");
return;
};
let text = format!("Approved command prefix saved:\n{prefixes}");
let message: ResponseItem = DeveloperInstructions::new(text.clone()).into();
let fragment = ApprovedCommandPrefixSaved::new(prefixes);
let text = fragment.render();
let message: ResponseItem = ContextualUserFragment::into(fragment);
if let Some(turn_context) = self.turn_context_for_sub_id(sub_id).await {
self.record_conversation_items(&turn_context, std::slice::from_ref(&message))
@@ -1747,15 +1753,9 @@ impl Session {
sub_id: &str,
amendment: &NetworkPolicyAmendment,
) {
let (action, list_name) = match amendment.action {
NetworkPolicyRuleAction::Allow => ("Allowed", "allowlist"),
NetworkPolicyRuleAction::Deny => ("Denied", "denylist"),
};
let text = format!(
"{action} network rule saved in execpolicy ({list_name}): {}",
amendment.host
);
let message: ResponseItem = DeveloperInstructions::new(text.clone()).into();
let fragment = NetworkRuleSaved::new(amendment);
let text = fragment.render();
let message: ResponseItem = ContextualUserFragment::into(fragment);
if let Some(turn_context) = self.turn_context_for_sub_id(sub_id).await {
self.record_conversation_items(&turn_context, std::slice::from_ref(&message))
@@ -2398,11 +2398,11 @@ impl Session {
turn_context,
)
{
developer_sections.push(model_switch_message.into_text());
developer_sections.push(model_switch_message);
}
if turn_context.config.include_permissions_instructions {
developer_sections.push(
DeveloperInstructions::from_policy(
PermissionsInstructions::from_policy(
turn_context.sandbox_policy.get(),
turn_context.approval_policy.value(),
turn_context.config.approvals_reviewer,
@@ -2415,7 +2415,7 @@ impl Session {
.features
.enabled(Feature::RequestPermissionsTool),
)
.into_text(),
.render(),
);
}
let separate_guardian_developer_message =
@@ -2438,16 +2438,16 @@ impl Session {
}
// Add developer instructions from collaboration_mode if they exist and are non-empty
if let Some(collab_instructions) =
DeveloperInstructions::from_collaboration_mode(&collaboration_mode)
CollaborationModeInstructions::from_collaboration_mode(&collaboration_mode)
{
developer_sections.push(collab_instructions.into_text());
developer_sections.push(collab_instructions.render());
}
if 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());
developer_sections.push(realtime_update);
}
if self.features.enabled(Feature::Personality)
&& let Some(personality) = turn_context.personality
@@ -2462,10 +2462,8 @@ impl Session {
personality,
)
{
developer_sections.push(
DeveloperInstructions::personality_spec_message(personality_message)
.into_text(),
);
developer_sections
.push(PersonalitySpecInstructions::new(personality_message).render());
}
}
if turn_context.config.include_apps_instructions && turn_context.apps_enabled() {
@@ -2476,8 +2474,10 @@ impl Session {
&turn_context.config,
)
.await;
if let Some(apps_section) = render_apps_section(&accessible_and_enabled_connectors) {
developer_sections.push(apps_section);
if let Some(apps_instructions) =
AppsInstructions::from_connectors(&accessible_and_enabled_connectors)
{
developer_sections.push(apps_instructions.render());
}
}
if turn_context.config.include_skill_instructions {
@@ -2485,15 +2485,17 @@ impl Session {
.turn_skills
.outcome
.allowed_skills_for_implicit_invocation();
let rendered_skills = render_skills_section(
let available_skills = build_available_skills(
&implicit_skills,
default_skill_metadata_budget(turn_context.model_info.context_window),
SkillRenderSideEffects::ThreadStart {
session_telemetry: &self.services.session_telemetry,
},
);
if let Some(rendered_skills) = rendered_skills {
if rendered_skills.emit_warning {
if let Some(available_skills) = available_skills {
let emit_warning = available_skills.emit_warning;
let skills_instructions = AvailableSkillsInstructions::from(available_skills);
if emit_warning {
self.send_event_raw(Event {
id: String::new(),
msg: EventMsg::Warning(WarningEvent {
@@ -2502,7 +2504,7 @@ impl Session {
})
.await;
}
developer_sections.push(rendered_skills.text);
developer_sections.push(skills_instructions.render());
}
}
let loaded_plugins = self
@@ -2510,9 +2512,10 @@ impl Session {
.plugins_manager
.plugins_for_config(&turn_context.config)
.await;
if let Some(plugin_section) = render_plugins_section(loaded_plugins.capability_summaries())
if let Some(plugin_instructions) =
AvailablePluginsInstructions::from_plugins(loaded_plugins.capability_summaries())
{
developer_sections.push(plugin_section);
developer_sections.push(plugin_instructions.render());
}
if turn_context.features.enabled(Feature::CodexGitCommit)
&& let Some(commit_message_instruction) = commit_message_trailer_instruction(
+10 -9
View File
@@ -88,7 +88,6 @@ use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::BaseInstructions;
use codex_protocol::models::ContentItem;
use codex_protocol::models::DeveloperInstructions;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::AskForApproval;
@@ -5026,7 +5025,7 @@ async fn build_initial_context_trims_skill_metadata_from_context_window_budget()
#[test]
fn emit_thread_start_skill_metrics_records_enabled_kept_and_truncated_values() {
let session_telemetry = test_session_telemetry_without_metadata();
let rendered = render_skills_section(
let rendered = build_available_skills(
&[SkillMetadata {
name: "repo-skill".to_string(),
description: "desc".to_string(),
@@ -5147,12 +5146,12 @@ async fn handle_output_item_done_records_image_save_history_message() {
let image_output_dir = image_output_path
.parent()
.expect("generated image path should have a parent");
let image_message: ResponseItem = DeveloperInstructions::new(format!(
"Generated images are saved to {} as {} by default.\nIf you need to use a generated image at another path, copy it and leave the original in place unless the user explicitly asks you to delete it.",
image_output_dir.display(),
image_output_path.display(),
))
.into();
let image_message: ResponseItem = crate::context::ContextualUserFragment::into(
crate::context::ImageGenerationInstructions::new(
image_output_dir.display(),
image_output_path.display(),
),
);
assert_eq!(history.raw_items(), &[image_message, item]);
assert_eq!(
std::fs::read(&expected_saved_path).expect("saved file"),
@@ -6449,7 +6448,9 @@ async fn sample_rollout(
.as_ref()
.and_then(|m| m.get_personality_message(Some(p)).filter(|s| !s.is_empty()))
{
let msg = DeveloperInstructions::personality_spec_message(personality_message).into();
let msg = crate::context::ContextualUserFragment::into(
crate::context::PersonalitySpecInstructions::new(personality_message),
);
let insert_at = initial_context
.iter()
.position(|m| matches!(m, ResponseItem::Message { role, .. } if role == "developer"))
+1 -1
View File
@@ -24,6 +24,7 @@ pub use codex_core_skills::SkillPolicy;
pub use codex_core_skills::SkillRenderReport;
pub use codex_core_skills::SkillsLoadInput;
pub use codex_core_skills::SkillsManager;
pub use codex_core_skills::build_available_skills;
pub use codex_core_skills::build_skill_name_counts;
pub use codex_core_skills::collect_env_var_dependencies;
pub use codex_core_skills::config_rules;
@@ -40,7 +41,6 @@ pub use codex_core_skills::model;
pub use codex_core_skills::remote;
pub use codex_core_skills::render;
pub use codex_core_skills::render::SkillRenderSideEffects;
pub use codex_core_skills::render_skills_section;
pub use codex_core_skills::system;
pub(crate) fn skills_load_input_from_config(
+7 -7
View File
@@ -8,6 +8,8 @@ use codex_protocol::items::TurnItem;
use codex_utils_stream_parser::strip_citations;
use tokio_util::sync::CancellationToken;
use crate::context::ContextualUserFragment;
use crate::context::ImageGenerationInstructions;
use crate::function_tool::FunctionCallError;
use crate::memories::citations::parse_memory_citation;
use crate::memories::citations::thread_ids_from_memory_citation;
@@ -18,7 +20,6 @@ use crate::tools::parallel::ToolCallRuntime;
use crate::tools::router::ToolRouter;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result;
use codex_protocol::models::DeveloperInstructions;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::MessagePhase;
@@ -392,12 +393,11 @@ pub(crate) async fn handle_non_tool_response_item(
let image_output_dir = image_output_path
.parent()
.unwrap_or_else(|| turn_context.config.codex_home.clone());
let message: ResponseItem = DeveloperInstructions::new(format!(
"Generated images are saved to {} as {} by default.\nIf you need to use a generated image at another path, copy it and leave the original in place unless the user explicitly asks you to delete it.",
image_output_dir.display(),
image_output_path.display(),
))
.into();
let message: ResponseItem =
ContextualUserFragment::into(ImageGenerationInstructions::new(
image_output_dir.display(),
image_output_path.display(),
));
sess.record_conversation_items(turn_context, &[message])
.await;
}
@@ -5,17 +5,14 @@ use crate::agent::control::render_input_preview;
use crate::agent::next_thread_spawn_depth;
use crate::agent::role::DEFAULT_ROLE_NAME;
use crate::agent::role::apply_role_to_config;
use crate::context::ContextualUserFragment;
use crate::context::SpawnAgentInstructions;
use codex_protocol::AgentPath;
use codex_protocol::models::DeveloperInstructions;
use codex_protocol::protocol::InterAgentCommunication;
use codex_protocol::protocol::Op;
pub(crate) struct Handler;
pub(crate) const SPAWN_AGENT_DEVELOPER_INSTRUCTIONS: &str = r#"<spawned_agent_context>
You are a newly spawned agent in a team of agents collaborating to complete a task. You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents. You are responsible for returning the response to your assigned task in the final channel. When you give your response, the contents of your response in the final channel will be immediately delivered back to your parent agent. The prior conversation history was forked from your parent agent. Treat the next user message as your assigned task, and use the forked history only as background context.
</spawned_agent_context>"#;
impl ToolHandler for Handler {
type Output = SpawnAgentResult;
@@ -91,15 +88,16 @@ impl ToolHandler for Handler {
}
apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?;
apply_spawn_agent_overrides(&mut config, child_depth);
let spawn_agent_instructions = SpawnAgentInstructions.render();
config.developer_instructions = Some(
if let Some(existing_instructions) = config.developer_instructions.take() {
DeveloperInstructions::new(existing_instructions)
.concat(DeveloperInstructions::new(
SPAWN_AGENT_DEVELOPER_INSTRUCTIONS,
))
.into_text()
if let Some(mut existing_instructions) = config.developer_instructions.take() {
if !existing_instructions.ends_with('\n') {
existing_instructions.push('\n');
}
existing_instructions.push_str(&spawn_agent_instructions);
existing_instructions
} else {
DeveloperInstructions::new(SPAWN_AGENT_DEVELOPER_INSTRUCTIONS).into_text()
spawn_agent_instructions
},
);
@@ -1,8 +1,9 @@
use anyhow::Result;
use codex_core::ForkSnapshot;
use codex_core::config::Constrained;
use codex_execpolicy::Policy;
use codex_protocol::models::DeveloperInstructions;
use codex_core::context::ContextualUserFragment;
use codex_core::context::PermissionsInstructions;
use codex_core::load_exec_policy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
@@ -548,18 +549,18 @@ async fn permissions_message_includes_writable_roots() -> Result<()> {
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
let permissions = permissions_texts(&req.single_request());
let expected = DeveloperInstructions::from_policy(
let normalize_line_endings = |s: &str| s.replace("\r\n", "\n");
let exec_policy = load_exec_policy(&test.config.config_layer_stack).await?;
let expected = PermissionsInstructions::from_policy(
&sandbox_policy,
AskForApproval::OnRequest,
test.config.approvals_reviewer,
&Policy::empty(),
&exec_policy,
test.config.cwd.as_path(),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
)
.into_text();
// Normalize line endings to handle Windows vs Unix differences
let normalize_line_endings = |s: &str| s.replace("\r\n", "\n");
.render();
let expected_normalized = normalize_line_endings(&expected);
let actual_normalized: Vec<String> = permissions
.iter()
-822
View File
@@ -4,20 +4,15 @@ use std::io;
use std::num::NonZeroUsize;
use std::path::Path;
use std::path::PathBuf;
use std::sync::LazyLock;
use codex_utils_image::PromptImageMode;
use codex_utils_image::load_for_prompt_bytes;
use codex_utils_template::Template;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::ser::Serializer;
use ts_rs::TS;
use crate::config_types::ApprovalsReviewer;
use crate::config_types::CollaborationMode;
use crate::config_types::SandboxMode;
use crate::permissions::FileSystemAccessMode;
use crate::permissions::FileSystemPath;
use crate::permissions::FileSystemSandboxEntry;
@@ -25,36 +20,14 @@ use crate::permissions::FileSystemSandboxKind;
use crate::permissions::FileSystemSandboxPolicy;
use crate::permissions::FileSystemSpecialPath;
use crate::permissions::NetworkSandboxPolicy;
use crate::protocol::AskForApproval;
use crate::protocol::COLLABORATION_MODE_CLOSE_TAG;
use crate::protocol::COLLABORATION_MODE_OPEN_TAG;
use crate::protocol::GranularApprovalConfig;
use crate::protocol::NetworkAccess;
use crate::protocol::REALTIME_CONVERSATION_CLOSE_TAG;
use crate::protocol::REALTIME_CONVERSATION_OPEN_TAG;
use crate::protocol::SandboxPolicy;
use crate::protocol::WritableRoot;
use crate::user_input::UserInput;
use codex_execpolicy::Policy;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_image::ImageProcessingError;
use schemars::JsonSchema;
use crate::mcp::CallToolResult;
static SANDBOX_MODE_DANGER_FULL_ACCESS_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
Template::parse(SANDBOX_MODE_DANGER_FULL_ACCESS.trim_end())
.unwrap_or_else(|err| panic!("danger-full-access sandbox template must parse: {err}"))
});
static SANDBOX_MODE_WORKSPACE_WRITE_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
Template::parse(SANDBOX_MODE_WORKSPACE_WRITE.trim_end())
.unwrap_or_else(|err| panic!("workspace-write sandbox template must parse: {err}"))
});
static SANDBOX_MODE_READ_ONLY_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
Template::parse(SANDBOX_MODE_READ_ONLY.trim_end())
.unwrap_or_else(|err| panic!("read-only sandbox template must parse: {err}"))
});
type CommitID = String;
/// Details of a ghost commit created from a repository state.
@@ -636,345 +609,6 @@ impl Default for BaseInstructions {
}
}
/// Developer-provided guidance that is injected into a turn as a developer role
/// message.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
#[serde(rename = "developer_instructions", rename_all = "snake_case")]
pub struct DeveloperInstructions {
text: String,
}
const APPROVAL_POLICY_NEVER: &str = include_str!("prompts/permissions/approval_policy/never.md");
const APPROVAL_POLICY_UNLESS_TRUSTED: &str =
include_str!("prompts/permissions/approval_policy/unless_trusted.md");
const APPROVAL_POLICY_ON_FAILURE: &str =
include_str!("prompts/permissions/approval_policy/on_failure.md");
const APPROVAL_POLICY_ON_REQUEST_RULE: &str =
include_str!("prompts/permissions/approval_policy/on_request.md");
const APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION: &str =
include_str!("prompts/permissions/approval_policy/on_request_rule_request_permission.md");
const AUTO_REVIEW_APPROVAL_SUFFIX: &str = "`approvals_reviewer` is `auto_review`: Sandbox escalations with require_escalated will be reviewed for compliance with the policy. If a rejection happens, you should proceed only with a materially safer alternative, or inform the user of the risk and send a final message to ask for approval.";
const SANDBOX_MODE_DANGER_FULL_ACCESS: &str =
include_str!("prompts/permissions/sandbox_mode/danger_full_access.md");
const SANDBOX_MODE_WORKSPACE_WRITE: &str =
include_str!("prompts/permissions/sandbox_mode/workspace_write.md");
const SANDBOX_MODE_READ_ONLY: &str = include_str!("prompts/permissions/sandbox_mode/read_only.md");
const REALTIME_START_INSTRUCTIONS: &str = include_str!("prompts/realtime/realtime_start.md");
const REALTIME_END_INSTRUCTIONS: &str = include_str!("prompts/realtime/realtime_end.md");
struct PermissionsPromptConfig<'a> {
approval_policy: AskForApproval,
approvals_reviewer: ApprovalsReviewer,
exec_policy: &'a Policy,
exec_permission_approvals_enabled: bool,
request_permissions_tool_enabled: bool,
}
impl DeveloperInstructions {
pub fn new<T: Into<String>>(text: T) -> Self {
Self { text: text.into() }
}
pub fn from(
approval_policy: AskForApproval,
approvals_reviewer: ApprovalsReviewer,
exec_policy: &Policy,
exec_permission_approvals_enabled: bool,
request_permissions_tool_enabled: bool,
) -> DeveloperInstructions {
let with_request_permissions_tool = |text: &str| {
if request_permissions_tool_enabled {
format!("{text}\n\n{}", request_permissions_tool_prompt_section())
} else {
text.to_string()
}
};
let on_request_instructions = || {
let on_request_rule = if exec_permission_approvals_enabled {
APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION.to_string()
} else {
APPROVAL_POLICY_ON_REQUEST_RULE.to_string()
};
let mut sections = vec![on_request_rule];
if request_permissions_tool_enabled {
sections.push(request_permissions_tool_prompt_section().to_string());
}
if let Some(prefixes) = approved_command_prefixes_text(exec_policy) {
sections.push(format!(
"## Approved command prefixes\nThe following prefix rules have already been approved: {prefixes}"
));
}
sections.join("\n\n")
};
let text = match approval_policy {
AskForApproval::Never => APPROVAL_POLICY_NEVER.to_string(),
AskForApproval::UnlessTrusted => {
with_request_permissions_tool(APPROVAL_POLICY_UNLESS_TRUSTED)
}
AskForApproval::OnFailure => with_request_permissions_tool(APPROVAL_POLICY_ON_FAILURE),
AskForApproval::OnRequest => on_request_instructions(),
AskForApproval::Granular(granular_config) => granular_instructions(
granular_config,
exec_policy,
exec_permission_approvals_enabled,
request_permissions_tool_enabled,
),
};
let text = if approvals_reviewer == ApprovalsReviewer::GuardianSubagent
&& approval_policy != AskForApproval::Never
{
format!("{text}\n\n{AUTO_REVIEW_APPROVAL_SUFFIX}")
} else {
text
};
DeveloperInstructions::new(text)
}
pub fn into_text(self) -> String {
self.text
}
pub fn concat(self, other: impl Into<DeveloperInstructions>) -> Self {
let mut text = self.text;
if !text.ends_with('\n') {
text.push('\n');
}
text.push_str(&other.into().text);
Self { text }
}
pub fn model_switch_message(model_instructions: String) -> Self {
DeveloperInstructions::new(format!(
"<model_switch>\nThe user was previously using a different model. Please continue the conversation according to the following instructions:\n\n{model_instructions}\n</model_switch>"
))
}
pub fn realtime_start_message() -> Self {
Self::realtime_start_message_with_instructions(REALTIME_START_INSTRUCTIONS.trim())
}
pub fn realtime_start_message_with_instructions(instructions: &str) -> Self {
DeveloperInstructions::new(format!(
"{REALTIME_CONVERSATION_OPEN_TAG}\n{instructions}\n{REALTIME_CONVERSATION_CLOSE_TAG}"
))
}
pub fn realtime_end_message(reason: &str) -> Self {
DeveloperInstructions::new(format!(
"{REALTIME_CONVERSATION_OPEN_TAG}\n{}\n\nReason: {reason}\n{REALTIME_CONVERSATION_CLOSE_TAG}",
REALTIME_END_INSTRUCTIONS.trim()
))
}
pub fn personality_spec_message(spec: String) -> Self {
let message = format!(
"<personality_spec> The user has requested a new communication style. Future messages should adhere to the following personality: \n{spec} </personality_spec>"
);
DeveloperInstructions::new(message)
}
pub fn from_policy(
sandbox_policy: &SandboxPolicy,
approval_policy: AskForApproval,
approvals_reviewer: ApprovalsReviewer,
exec_policy: &Policy,
cwd: &Path,
exec_permission_approvals_enabled: bool,
request_permissions_tool_enabled: bool,
) -> Self {
let network_access = if sandbox_policy.has_full_network_access() {
NetworkAccess::Enabled
} else {
NetworkAccess::Restricted
};
let (sandbox_mode, writable_roots) = match sandbox_policy {
SandboxPolicy::DangerFullAccess => (SandboxMode::DangerFullAccess, None),
SandboxPolicy::ReadOnly { .. } => (SandboxMode::ReadOnly, None),
SandboxPolicy::ExternalSandbox { .. } => (SandboxMode::DangerFullAccess, None),
SandboxPolicy::WorkspaceWrite { .. } => {
let roots = sandbox_policy.get_writable_roots_with_cwd(cwd);
(SandboxMode::WorkspaceWrite, Some(roots))
}
};
DeveloperInstructions::from_permissions_with_network(
sandbox_mode,
network_access,
PermissionsPromptConfig {
approval_policy,
approvals_reviewer,
exec_policy,
exec_permission_approvals_enabled,
request_permissions_tool_enabled,
},
writable_roots,
)
}
/// Returns developer instructions from a collaboration mode if they exist and are non-empty.
pub fn from_collaboration_mode(collaboration_mode: &CollaborationMode) -> Option<Self> {
collaboration_mode
.settings
.developer_instructions
.as_ref()
.filter(|instructions| !instructions.is_empty())
.map(|instructions| {
DeveloperInstructions::new(format!(
"{COLLABORATION_MODE_OPEN_TAG}{instructions}{COLLABORATION_MODE_CLOSE_TAG}"
))
})
}
fn from_permissions_with_network(
sandbox_mode: SandboxMode,
network_access: NetworkAccess,
config: PermissionsPromptConfig<'_>,
writable_roots: Option<Vec<WritableRoot>>,
) -> Self {
let start_tag = DeveloperInstructions::new("<permissions instructions>");
let end_tag = DeveloperInstructions::new("</permissions instructions>");
start_tag
.concat(DeveloperInstructions::sandbox_text(
sandbox_mode,
network_access,
))
.concat(DeveloperInstructions::from(
config.approval_policy,
config.approvals_reviewer,
config.exec_policy,
config.exec_permission_approvals_enabled,
config.request_permissions_tool_enabled,
))
.concat(DeveloperInstructions::from_writable_roots(writable_roots))
.concat(end_tag)
}
fn from_writable_roots(writable_roots: Option<Vec<WritableRoot>>) -> Self {
let Some(roots) = writable_roots else {
return DeveloperInstructions::new("");
};
if roots.is_empty() {
return DeveloperInstructions::new("");
}
let roots_list: Vec<String> = roots
.iter()
.map(|r| format!("`{}`", r.root.to_string_lossy()))
.collect();
let text = if roots_list.len() == 1 {
format!(" The writable root is {}.", roots_list[0])
} else {
format!(" The writable roots are {}.", roots_list.join(", "))
};
DeveloperInstructions::new(text)
}
fn sandbox_text(mode: SandboxMode, network_access: NetworkAccess) -> DeveloperInstructions {
let template = match mode {
SandboxMode::DangerFullAccess => &*SANDBOX_MODE_DANGER_FULL_ACCESS_TEMPLATE,
SandboxMode::WorkspaceWrite => &*SANDBOX_MODE_WORKSPACE_WRITE_TEMPLATE,
SandboxMode::ReadOnly => &*SANDBOX_MODE_READ_ONLY_TEMPLATE,
};
let network_access = network_access.to_string();
let text = template
.render([("network_access", network_access.as_str())])
.unwrap_or_else(|err| panic!("sandbox template must render: {err}"));
DeveloperInstructions::new(text)
}
}
fn approved_command_prefixes_text(exec_policy: &Policy) -> Option<String> {
format_allow_prefixes(exec_policy.get_allowed_prefixes())
.filter(|prefixes| !prefixes.is_empty())
}
fn granular_prompt_intro_text() -> &'static str {
"# Approval Requests\n\nApproval policy is `granular`. Categories set to `false` are automatically rejected instead of prompting the user."
}
fn request_permissions_tool_prompt_section() -> &'static str {
"# request_permissions Tool\n\nThe built-in `request_permissions` tool is available in this session. Invoke it when you need to request additional `network` or `file_system` permissions before later shell-like commands need them. Request only the specific permissions required for the task."
}
fn granular_instructions(
granular_config: GranularApprovalConfig,
exec_policy: &Policy,
exec_permission_approvals_enabled: bool,
request_permissions_tool_enabled: bool,
) -> String {
let sandbox_approval_prompts_allowed = granular_config.allows_sandbox_approval();
let shell_permission_requests_available =
exec_permission_approvals_enabled && sandbox_approval_prompts_allowed;
let request_permissions_tool_prompts_allowed =
request_permissions_tool_enabled && granular_config.allows_request_permissions();
let categories = [
Some((
granular_config.allows_sandbox_approval(),
"`sandbox_approval`",
)),
Some((granular_config.allows_rules_approval(), "`rules`")),
Some((granular_config.allows_skill_approval(), "`skill_approval`")),
request_permissions_tool_enabled.then_some((
granular_config.allows_request_permissions(),
"`request_permissions`",
)),
Some((
granular_config.allows_mcp_elicitations(),
"`mcp_elicitations`",
)),
];
let prompted_categories = categories
.iter()
.flatten()
.filter(|&&(is_allowed, _)| is_allowed)
.map(|&(_, category)| format!("- {category}"))
.collect::<Vec<_>>();
let rejected_categories = categories
.iter()
.flatten()
.filter(|&&(is_allowed, _)| !is_allowed)
.map(|&(_, category)| format!("- {category}"))
.collect::<Vec<_>>();
let mut sections = vec![granular_prompt_intro_text().to_string()];
if !prompted_categories.is_empty() {
sections.push(format!(
"These approval categories may still prompt the user when needed:\n{}",
prompted_categories.join("\n")
));
}
if !rejected_categories.is_empty() {
sections.push(format!(
"These approval categories are automatically rejected instead of prompting the user:\n{}",
rejected_categories.join("\n")
));
}
if shell_permission_requests_available {
sections.push(APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION.to_string());
}
if request_permissions_tool_prompts_allowed {
sections.push(request_permissions_tool_prompt_section().to_string());
}
if let Some(prefixes) = approved_command_prefixes_text(exec_policy) {
sections.push(format!(
"## Approved command prefixes\nThe following prefix rules have already been approved: {prefixes}"
));
}
sections.join("\n\n")
}
const MAX_RENDERED_PREFIXES: usize = 100;
const MAX_ALLOW_PREFIX_TEXT_BYTES: usize = 5000;
const TRUNCATED_MARKER: &str = "...\n[Some commands were truncated]";
@@ -1031,31 +665,6 @@ fn render_command_prefix(prefix: &[String]) -> String {
format!("[{tokens}]")
}
impl From<DeveloperInstructions> for ResponseItem {
fn from(di: DeveloperInstructions) -> Self {
ResponseItem::Message {
id: None,
role: "developer".to_string(),
content: vec![ContentItem::InputText {
text: di.into_text(),
}],
end_turn: None,
phase: None,
}
}
}
impl From<SandboxMode> for DeveloperInstructions {
fn from(mode: SandboxMode) -> Self {
let network_access = match mode {
SandboxMode::DangerFullAccess => NetworkAccess::Enabled,
SandboxMode::WorkspaceWrite | SandboxMode::ReadOnly => NetworkAccess::Restricted,
};
DeveloperInstructions::sandbox_text(mode, network_access)
}
}
fn should_serialize_reasoning_content(content: &Option<Vec<ReasoningItemContent>>) -> bool {
match content {
Some(content) => !content
@@ -1730,13 +1339,9 @@ impl std::fmt::Display for FunctionCallOutputPayload {
#[cfg(test)]
mod tests {
use super::*;
use crate::config_types::SandboxMode;
use crate::protocol::AskForApproval;
use crate::protocol::GranularApprovalConfig;
use anyhow::Result;
use codex_execpolicy::Policy;
use pretty_assertions::assert_eq;
use std::path::PathBuf;
use tempfile::tempdir;
#[test]
@@ -2021,433 +1626,6 @@ mod tests {
);
}
#[test]
fn converts_sandbox_mode_into_developer_instructions() {
let workspace_write: DeveloperInstructions = SandboxMode::WorkspaceWrite.into();
assert_eq!(
workspace_write,
DeveloperInstructions::new(
"Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: The sandbox permits reading files, and editing files in `cwd` and `writable_roots`. Editing files in other directories requires approval. Network access is restricted."
)
);
let read_only: DeveloperInstructions = SandboxMode::ReadOnly.into();
assert_eq!(
read_only,
DeveloperInstructions::new(
"Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `read-only`: The sandbox only permits reading files. Network access is restricted."
)
);
let danger_full_access: DeveloperInstructions = SandboxMode::DangerFullAccess.into();
assert_eq!(
danger_full_access,
DeveloperInstructions::new(
"Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`: No filesystem sandboxing - all commands are permitted. Network access is enabled."
)
);
}
#[test]
fn builds_permissions_with_network_access_override() {
let instructions = DeveloperInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Enabled,
PermissionsPromptConfig {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: false,
request_permissions_tool_enabled: false,
},
/*writable_roots*/ None,
);
let text = instructions.into_text();
assert!(
text.contains("Network access is enabled."),
"expected network access to be enabled in message"
);
assert!(
text.contains("How to request escalation"),
"expected approval guidance to be included"
);
}
#[test]
fn builds_permissions_from_policy() {
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
read_only_access: Default::default(),
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};
let instructions = DeveloperInstructions::from_policy(
&policy,
AskForApproval::UnlessTrusted,
ApprovalsReviewer::User,
&Policy::empty(),
&PathBuf::from("/tmp"),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
);
let text = instructions.into_text();
assert!(text.contains("Network access is enabled."));
assert!(text.contains("`approval_policy` is `unless-trusted`"));
}
#[test]
fn includes_request_rule_instructions_for_on_request() {
let mut exec_policy = Policy::empty();
exec_policy
.add_prefix_rule(
&["git".to_string(), "pull".to_string()],
codex_execpolicy::Decision::Allow,
)
.expect("add rule");
let instructions = DeveloperInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Enabled,
PermissionsPromptConfig {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
exec_policy: &exec_policy,
exec_permission_approvals_enabled: false,
request_permissions_tool_enabled: false,
},
/*writable_roots*/ None,
);
let text = instructions.into_text();
assert!(text.contains("prefix_rule"));
assert!(text.contains("Approved command prefixes"));
assert!(text.contains(r#"["git", "pull"]"#));
}
#[test]
fn includes_request_permissions_tool_instructions_for_unless_trusted_when_enabled() {
let instructions = DeveloperInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Enabled,
PermissionsPromptConfig {
approval_policy: AskForApproval::UnlessTrusted,
approvals_reviewer: ApprovalsReviewer::User,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: false,
request_permissions_tool_enabled: true,
},
/*writable_roots*/ None,
);
let text = instructions.into_text();
assert!(text.contains("`approval_policy` is `unless-trusted`"));
assert!(text.contains("# request_permissions Tool"));
}
#[test]
fn includes_request_permissions_tool_instructions_for_on_failure_when_enabled() {
let instructions = DeveloperInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Enabled,
PermissionsPromptConfig {
approval_policy: AskForApproval::OnFailure,
approvals_reviewer: ApprovalsReviewer::User,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: false,
request_permissions_tool_enabled: true,
},
/*writable_roots*/ None,
);
let text = instructions.into_text();
assert!(text.contains("`approval_policy` is `on-failure`"));
assert!(text.contains("# request_permissions Tool"));
}
#[test]
fn includes_request_permission_rule_instructions_for_on_request_when_enabled() {
let instructions = DeveloperInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Enabled,
PermissionsPromptConfig {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: true,
request_permissions_tool_enabled: false,
},
/*writable_roots*/ None,
);
let text = instructions.into_text();
assert!(text.contains("with_additional_permissions"));
assert!(text.contains("additional_permissions"));
}
#[test]
fn includes_request_permissions_tool_instructions_for_on_request_when_tool_is_enabled() {
let instructions = DeveloperInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Enabled,
PermissionsPromptConfig {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: false,
request_permissions_tool_enabled: true,
},
/*writable_roots*/ None,
);
let text = instructions.into_text();
assert!(text.contains("# request_permissions Tool"));
assert!(
text.contains("The built-in `request_permissions` tool is available in this session.")
);
}
#[test]
fn on_request_includes_tool_guidance_alongside_inline_permission_guidance_when_both_exist() {
let instructions = DeveloperInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Enabled,
PermissionsPromptConfig {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: true,
request_permissions_tool_enabled: true,
},
/*writable_roots*/ None,
);
let text = instructions.into_text();
assert!(text.contains("with_additional_permissions"));
assert!(text.contains("# request_permissions Tool"));
}
#[test]
fn guardian_subagent_approvals_append_guardian_specific_guidance() {
let text = DeveloperInstructions::from(
AskForApproval::OnRequest,
ApprovalsReviewer::GuardianSubagent,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
)
.into_text();
assert!(text.contains("`approvals_reviewer` is `auto_review`"));
assert!(!text.contains("`approvals_reviewer` is `guardian_subagent`"));
assert!(text.contains("materially safer alternative"));
}
#[test]
fn guardian_subagent_approvals_omit_guardian_specific_guidance_when_approval_is_never() {
let text = DeveloperInstructions::from(
AskForApproval::Never,
ApprovalsReviewer::GuardianSubagent,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
)
.into_text();
assert!(!text.contains("`approvals_reviewer` is `auto_review`"));
assert!(!text.contains("`approvals_reviewer` is `guardian_subagent`"));
}
fn granular_categories_section(title: &str, categories: &[&str]) -> String {
format!("{title}\n{}", categories.join("\n"))
}
fn granular_prompt_expected(
prompted_categories: &[&str],
rejected_categories: &[&str],
include_shell_permission_request_instructions: bool,
include_request_permissions_tool_section: bool,
) -> String {
let mut sections = vec![granular_prompt_intro_text().to_string()];
if !prompted_categories.is_empty() {
sections.push(granular_categories_section(
"These approval categories may still prompt the user when needed:",
prompted_categories,
));
}
if !rejected_categories.is_empty() {
sections.push(granular_categories_section(
"These approval categories are automatically rejected instead of prompting the user:",
rejected_categories,
));
}
if include_shell_permission_request_instructions {
sections.push(APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION.to_string());
}
if include_request_permissions_tool_section {
sections.push(request_permissions_tool_prompt_section().to_string());
}
sections.join("\n\n")
}
#[test]
fn granular_policy_lists_prompted_and_rejected_categories_separately() {
let text = DeveloperInstructions::from(
AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: false,
rules: true,
skill_approval: false,
request_permissions: true,
mcp_elicitations: false,
}),
ApprovalsReviewer::User,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ false,
)
.into_text();
assert_eq!(
text,
[
granular_prompt_intro_text().to_string(),
granular_categories_section(
"These approval categories may still prompt the user when needed:",
&["- `rules`"],
),
granular_categories_section(
"These approval categories are automatically rejected instead of prompting the user:",
&["- `sandbox_approval`", "- `skill_approval`", "- `mcp_elicitations`",],
),
]
.join("\n\n")
);
}
#[test]
fn granular_policy_includes_command_permission_instructions_when_sandbox_approval_can_prompt() {
let text = DeveloperInstructions::from(
AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: true,
rules: true,
skill_approval: true,
request_permissions: true,
mcp_elicitations: true,
}),
ApprovalsReviewer::User,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ false,
)
.into_text();
assert_eq!(
text,
granular_prompt_expected(
&[
"- `sandbox_approval`",
"- `rules`",
"- `skill_approval`",
"- `mcp_elicitations`",
],
&[],
/*include_shell_permission_request_instructions*/ true,
/*include_request_permissions_tool_section*/ false,
)
);
}
#[test]
fn granular_policy_omits_shell_permission_instructions_when_inline_requests_are_disabled() {
let text = DeveloperInstructions::from(
AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: true,
rules: true,
skill_approval: true,
request_permissions: true,
mcp_elicitations: true,
}),
ApprovalsReviewer::User,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
)
.into_text();
assert_eq!(
text,
granular_prompt_expected(
&[
"- `sandbox_approval`",
"- `rules`",
"- `skill_approval`",
"- `mcp_elicitations`",
],
&[],
/*include_shell_permission_request_instructions*/ false,
/*include_request_permissions_tool_section*/ false,
)
);
}
#[test]
fn granular_policy_includes_request_permissions_tool_only_when_that_prompt_can_still_fire() {
let allowed = DeveloperInstructions::from(
AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: true,
rules: true,
skill_approval: true,
request_permissions: true,
mcp_elicitations: true,
}),
ApprovalsReviewer::User,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ true,
)
.into_text();
assert!(allowed.contains("# request_permissions Tool"));
let rejected = DeveloperInstructions::from(
AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: true,
rules: true,
skill_approval: true,
request_permissions: false,
mcp_elicitations: true,
}),
ApprovalsReviewer::User,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ true,
)
.into_text();
assert!(!rejected.contains("# request_permissions Tool"));
}
#[test]
fn granular_policy_lists_request_permissions_category_without_tool_section_when_tool_is_unavailable()
{
let text = DeveloperInstructions::from(
AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: false,
rules: false,
skill_approval: false,
request_permissions: true,
mcp_elicitations: false,
}),
ApprovalsReviewer::User,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ false,
)
.into_text();
assert!(!text.contains("- `request_permissions`"));
assert!(!text.contains("# request_permissions Tool"));
}
#[test]
fn render_command_prefix_list_sorts_by_len_then_total_len_then_alphabetical() {
let prefixes = vec![