From 2a226096f60450bb9978d02e030ce29283f00c55 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Tue, 21 Apr 2026 10:22:36 -0700 Subject: [PATCH] Split DeveloperInstructions into individual fragments. (#18813) Split DeveloperInstructions into individual fragments. --- .codex/skills/code-review-context/SKILL.md | 1 + codex-rs/core-skills/src/lib.rs | 4 +- codex-rs/core-skills/src/render.rs | 63 +- codex-rs/core/src/apps/mod.rs | 3 +- codex-rs/core/src/apps/render.rs | 17 +- .../context/approved_command_prefix_saved.rs | 24 + .../core/src/context/apps_instructions.rs | 30 + .../context/available_plugins_instructions.rs | 58 ++ .../context/available_skills_instructions.rs | 56 ++ .../collaboration_mode_instructions.rs | 32 + .../core/src/context/environment_context.rs | 2 +- codex-rs/core/src/context/fragment.rs | 30 +- .../guardian_followup_review_reminder.rs | 21 + .../src/context/hook_additional_context.rs | 22 + .../context/image_generation_instructions.rs | 30 + codex-rs/core/src/context/mod.rs | 38 +- .../src/context/model_switch_instructions.rs | 27 + .../core/src/context/network_rule_saved.rs | 35 + .../src/context/permissions_instructions.rs | 316 +++++++ .../context/permissions_instructions_tests.rs | 412 +++++++++ .../context/personality_spec_instructions.rs | 25 + .../core/src/context/plugin_instructions.rs | 22 + .../permissions/approval_policy/never.md | 0 .../permissions/approval_policy/on_failure.md | 0 .../permissions/approval_policy/on_request.md | 0 .../on_request_rule_request_permission.md | 0 .../approval_policy/unless_trusted.md | 0 .../sandbox_mode/danger_full_access.md | 0 .../permissions/sandbox_mode/read_only.md | 0 .../sandbox_mode/workspace_write.md | 0 .../context}/prompts/realtime/realtime_end.md | 0 .../prompts/realtime/realtime_start.md | 0 .../src/context/realtime_end_instructions.rs | 32 + .../context/realtime_start_instructions.rs | 18 + .../realtime_start_with_instructions.rs | 26 + .../core/src/context/skill_instructions.rs | 2 +- .../src/context/spawn_agent_instructions.rs | 14 + .../core/src/context/subagent_notification.rs | 2 +- codex-rs/core/src/context/turn_aborted.rs | 2 +- .../core/src/context/user_instructions.rs | 2 +- .../core/src/context/user_shell_command.rs | 2 +- codex-rs/core/src/context_manager/updates.rs | 63 +- codex-rs/core/src/guardian/review_session.rs | 14 +- codex-rs/core/src/hook_runtime.rs | 6 +- codex-rs/core/src/lib.rs | 4 +- codex-rs/core/src/plugins/injection.rs | 7 +- codex-rs/core/src/plugins/mod.rs | 1 - codex-rs/core/src/plugins/render.rs | 42 +- codex-rs/core/src/session/mod.rs | 69 +- codex-rs/core/src/session/tests.rs | 19 +- codex-rs/core/src/skills.rs | 2 +- codex-rs/core/src/stream_events_utils.rs | 14 +- .../tools/handlers/multi_agents_v2/spawn.rs | 22 +- .../core/tests/suite/permissions_messages.rs | 15 +- codex-rs/protocol/src/models.rs | 822 ------------------ 55 files changed, 1410 insertions(+), 1058 deletions(-) create mode 100644 codex-rs/core/src/context/approved_command_prefix_saved.rs create mode 100644 codex-rs/core/src/context/apps_instructions.rs create mode 100644 codex-rs/core/src/context/available_plugins_instructions.rs create mode 100644 codex-rs/core/src/context/available_skills_instructions.rs create mode 100644 codex-rs/core/src/context/collaboration_mode_instructions.rs create mode 100644 codex-rs/core/src/context/guardian_followup_review_reminder.rs create mode 100644 codex-rs/core/src/context/hook_additional_context.rs create mode 100644 codex-rs/core/src/context/image_generation_instructions.rs create mode 100644 codex-rs/core/src/context/model_switch_instructions.rs create mode 100644 codex-rs/core/src/context/network_rule_saved.rs create mode 100644 codex-rs/core/src/context/permissions_instructions.rs create mode 100644 codex-rs/core/src/context/permissions_instructions_tests.rs create mode 100644 codex-rs/core/src/context/personality_spec_instructions.rs create mode 100644 codex-rs/core/src/context/plugin_instructions.rs rename codex-rs/{protocol/src => core/src/context}/prompts/permissions/approval_policy/never.md (100%) rename codex-rs/{protocol/src => core/src/context}/prompts/permissions/approval_policy/on_failure.md (100%) rename codex-rs/{protocol/src => core/src/context}/prompts/permissions/approval_policy/on_request.md (100%) rename codex-rs/{protocol/src => core/src/context}/prompts/permissions/approval_policy/on_request_rule_request_permission.md (100%) rename codex-rs/{protocol/src => core/src/context}/prompts/permissions/approval_policy/unless_trusted.md (100%) rename codex-rs/{protocol/src => core/src/context}/prompts/permissions/sandbox_mode/danger_full_access.md (100%) rename codex-rs/{protocol/src => core/src/context}/prompts/permissions/sandbox_mode/read_only.md (100%) rename codex-rs/{protocol/src => core/src/context}/prompts/permissions/sandbox_mode/workspace_write.md (100%) rename codex-rs/{protocol/src => core/src/context}/prompts/realtime/realtime_end.md (100%) rename codex-rs/{protocol/src => core/src/context}/prompts/realtime/realtime_start.md (100%) create mode 100644 codex-rs/core/src/context/realtime_end_instructions.rs create mode 100644 codex-rs/core/src/context/realtime_start_instructions.rs create mode 100644 codex-rs/core/src/context/realtime_start_with_instructions.rs create mode 100644 codex-rs/core/src/context/spawn_agent_instructions.rs diff --git a/.codex/skills/code-review-context/SKILL.md b/.codex/skills/code-review-context/SKILL.md index 3d33192ec..7faf3d7cd 100644 --- a/.codex/skills/code-review-context/SKILL.md +++ b/.codex/skills/code-review-context/SKILL.md @@ -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 \ No newline at end of file diff --git a/codex-rs/core-skills/src/lib.rs b/codex-rs/core-skills/src/lib.rs index 6d3ac7a4f..06ced0d5d 100644 --- a/codex-rs/core-skills/src/lib.rs +++ b/codex-rs/core-skills/src/lib.rs @@ -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; diff --git a/codex-rs/core-skills/src/render.rs b/codex-rs/core-skills/src/render.rs index ad674fbff..6bcd5e4e0 100644 --- a/codex-rs/core-skills/src/render.rs +++ b/codex-rs/core-skills/src/render.rs @@ -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, pub report: SkillRenderReport, pub emit_warning: bool, } @@ -71,11 +69,11 @@ pub fn default_skill_metadata_budget(context_window: Option) -> SkillMetada )) } -pub fn render_skills_section( +pub fn build_available_skills( skills: &[SkillMetadata], budget: SkillMetadataBudget, side_effects: SkillRenderSideEffects<'_>, -) -> Option { +) -> Option { 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 = 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:")); } } diff --git a/codex-rs/core/src/apps/mod.rs b/codex-rs/core/src/apps/mod.rs index dcb5c4c20..5a58d2220 100644 --- a/codex-rs/core/src/apps/mod.rs +++ b/codex-rs/core/src/apps/mod.rs @@ -1,3 +1,2 @@ +#[cfg(test)] mod render; - -pub(crate) use render::render_apps_section; diff --git a/codex-rs/core/src/apps/render.rs b/codex-rs/core/src/apps/render.rs index fe23a09f8..3793231e1 100644 --- a/codex-rs/core/src/apps/render.rs +++ b/codex-rs/core/src/apps/render.rs @@ -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 { - 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)] diff --git a/codex-rs/core/src/context/approved_command_prefix_saved.rs b/codex-rs/core/src/context/approved_command_prefix_saved.rs new file mode 100644 index 000000000..3aac17712 --- /dev/null +++ b/codex-rs/core/src/context/approved_command_prefix_saved.rs @@ -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) -> 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) + } +} diff --git a/codex-rs/core/src/context/apps_instructions.rs b/codex-rs/core/src/context/apps_instructions.rs new file mode 100644 index 000000000..55865b429 --- /dev/null +++ b/codex-rs/core/src/context/apps_instructions.rs @@ -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 { + 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" + ) + } +} diff --git a/codex-rs/core/src/context/available_plugins_instructions.rs b/codex-rs/core/src/context/available_plugins_instructions.rs new file mode 100644 index 000000000..52f9c8df4 --- /dev/null +++ b/codex-rs/core/src/context/available_plugins_instructions.rs @@ -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, +} + +impl AvailablePluginsInstructions { + pub(crate) fn from_plugins(plugins: &[PluginCapabilitySummary]) -> Option { + 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")) + } +} diff --git a/codex-rs/core/src/context/available_skills_instructions.rs b/codex-rs/core/src/context/available_skills_instructions.rs new file mode 100644 index 000000000..aba4b2013 --- /dev/null +++ b/codex-rs/core/src/context/available_skills_instructions.rs @@ -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, +} + +impl From 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 = 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")) + } +} diff --git a/codex-rs/core/src/context/collaboration_mode_instructions.rs b/codex-rs/core/src/context/collaboration_mode_instructions.rs new file mode 100644 index 000000000..dcf3bac1d --- /dev/null +++ b/codex-rs/core/src/context/collaboration_mode_instructions.rs @@ -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 { + 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() + } +} diff --git a/codex-rs/core/src/context/environment_context.rs b/codex-rs/core/src/context/environment_context.rs index c676bfb9d..c4e77624f 100644 --- a/codex-rs/core/src/context/environment_context.rs +++ b/codex-rs/core/src/context/environment_context.rs @@ -200,7 +200,7 @@ impl ContextualUserFragment for EnvironmentContext { lines.extend(subagents.lines().map(|line| format!(" {line}"))); lines.push(" ".to_string()); } - format!("\n{}", lines.join("\n")) + format!("\n{}\n", lines.join("\n")) } } diff --git a/codex-rs/core/src/context/fragment.rs b/codex-rs/core/src/context/fragment.rs index e0e6d0330..34f4a7c36 100644 --- a/codex-rs/core/src/context/fragment.rs +++ b/codex-rs/core/src/context/fragment.rs @@ -28,13 +28,16 @@ impl 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 diff --git a/codex-rs/core/src/context/guardian_followup_review_reminder.rs b/codex-rs/core/src/context/guardian_followup_review_reminder.rs new file mode 100644 index 000000000..972355ba9 --- /dev/null +++ b/codex-rs/core/src/context/guardian_followup_review_reminder.rs @@ -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() + } +} diff --git a/codex-rs/core/src/context/hook_additional_context.rs b/codex-rs/core/src/context/hook_additional_context.rs new file mode 100644 index 000000000..01c95a652 --- /dev/null +++ b/codex-rs/core/src/context/hook_additional_context.rs @@ -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) -> 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() + } +} diff --git a/codex-rs/core/src/context/image_generation_instructions.rs b/codex-rs/core/src/context/image_generation_instructions.rs new file mode 100644 index 000000000..01abee909 --- /dev/null +++ b/codex-rs/core/src/context/image_generation_instructions.rs @@ -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 + ) + } +} diff --git a/codex-rs/core/src/context/mod.rs b/codex-rs/core/src/context/mod.rs index 94c5849f9..bbc5bcacf 100644 --- a/codex-rs/core/src/context/mod.rs +++ b/codex-rs/core/src/context/mod.rs @@ -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; diff --git a/codex-rs/core/src/context/model_switch_instructions.rs b/codex-rs/core/src/context/model_switch_instructions.rs new file mode 100644 index 000000000..0b75afb61 --- /dev/null +++ b/codex-rs/core/src/context/model_switch_instructions.rs @@ -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) -> Self { + Self { + model_instructions: model_instructions.into(), + } + } +} + +impl ContextualUserFragment for ModelSwitchInstructions { + const ROLE: &'static str = "developer"; + const START_MARKER: &'static str = ""; + const END_MARKER: &'static str = ""; + + 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 + ) + } +} diff --git a/codex-rs/core/src/context/network_rule_saved.rs b/codex-rs/core/src/context/network_rule_saved.rs new file mode 100644 index 000000000..8162e6bda --- /dev/null +++ b/codex-rs/core/src/context/network_rule_saved.rs @@ -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 + ) + } +} diff --git a/codex-rs/core/src/context/permissions_instructions.rs b/codex-rs/core/src/context/permissions_instructions.rs new file mode 100644 index 000000000..db420f746 --- /dev/null +++ b/codex-rs/core/src/context/permissions_instructions.rs @@ -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