From dcf0f741994db2631d596a4f5bc060be80dbc02a Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 27 May 2026 20:41:07 -0700 Subject: [PATCH] Register hidden context markers from extensions --- codex-rs/Cargo.lock | 2 ++ .../src/context/contextual_user_message.rs | 14 ++++---- .../context/contextual_user_message_tests.rs | 7 ++-- codex-rs/core/src/context/goal_context.rs | 29 ---------------- codex-rs/core/src/context/mod.rs | 2 -- codex-rs/core/src/tasks/idle_extension.rs | 4 +-- codex-rs/ext/extension-api/Cargo.toml | 1 + codex-rs/ext/extension-api/src/context.rs | 34 +++++++++++++++++++ codex-rs/ext/extension-api/src/lib.rs | 2 ++ codex-rs/ext/goal/Cargo.toml | 1 + codex-rs/ext/goal/src/steering.rs | 7 ++++ .../ext/goal/tests/goal_extension_backend.rs | 9 +++++ 12 files changed, 70 insertions(+), 42 deletions(-) delete mode 100644 codex-rs/core/src/context/goal_context.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 3935ad957e..53ce1f7605 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2848,6 +2848,7 @@ dependencies = [ "async-trait", "codex-protocol", "codex-tools", + "inventory", ] [[package]] @@ -2976,6 +2977,7 @@ dependencies = [ "codex-state", "codex-tools", "codex-utils-template", + "inventory", "pretty_assertions", "serde", "serde_json", diff --git a/codex-rs/core/src/context/contextual_user_message.rs b/codex-rs/core/src/context/contextual_user_message.rs index 2a7ee4d674..6399773835 100644 --- a/codex-rs/core/src/context/contextual_user_message.rs +++ b/codex-rs/core/src/context/contextual_user_message.rs @@ -6,7 +6,6 @@ use super::AdditionalContextUserFragment; use super::EnvironmentContext; use super::FragmentRegistration; use super::FragmentRegistrationProxy; -use super::GoalContext; use super::LegacyApplyPatchExecCommandWarning; use super::LegacyModelMismatchWarning; use super::LegacyUnifiedExecProcessLimitWarning; @@ -22,8 +21,6 @@ static ENVIRONMENT_CONTEXT_REGISTRATION: FragmentRegistrationProxy = FragmentRegistrationProxy::new(); -static GOAL_CONTEXT_REGISTRATION: FragmentRegistrationProxy = - FragmentRegistrationProxy::new(); static SKILL_INSTRUCTIONS_REGISTRATION: FragmentRegistrationProxy = FragmentRegistrationProxy::new(); static USER_SHELL_COMMAND_REGISTRATION: FragmentRegistrationProxy = @@ -46,7 +43,6 @@ static CONTEXTUAL_USER_FRAGMENTS: &[&dyn FragmentRegistration] = &[ &USER_INSTRUCTIONS_REGISTRATION, &ENVIRONMENT_CONTEXT_REGISTRATION, &ADDITIONAL_CONTEXT_REGISTRATION, - &GOAL_CONTEXT_REGISTRATION, &SKILL_INSTRUCTIONS_REGISTRATION, &USER_SHELL_COMMAND_REGISTRATION, &TURN_ABORTED_REGISTRATION, @@ -62,11 +58,17 @@ fn is_standard_contextual_user_text(text: &str) -> bool { .any(|fragment| fragment.matches_text(text)) } +fn is_registered_extension_context_text(text: &str) -> bool { + codex_extension_api::registered_hidden_context_markers().any(|marker| marker.matches_text(text)) +} + pub(crate) fn is_contextual_user_fragment(content_item: &ContentItem) -> bool { let ContentItem::InputText { text } = content_item else { return false; }; - parse_hook_prompt_fragment(text).is_some() || is_standard_contextual_user_text(text) + parse_hook_prompt_fragment(text).is_some() + || is_standard_contextual_user_text(text) + || is_registered_extension_context_text(text) } pub(crate) fn parse_visible_hook_prompt_message( @@ -83,7 +85,7 @@ pub(crate) fn parse_visible_hook_prompt_message( fragments.push(fragment); continue; } - if is_standard_contextual_user_text(text) { + if is_standard_contextual_user_text(text) || is_registered_extension_context_text(text) { continue; } return None; diff --git a/codex-rs/core/src/context/contextual_user_message_tests.rs b/codex-rs/core/src/context/contextual_user_message_tests.rs index cfba3c2320..55a1bbb7d2 100644 --- a/codex-rs/core/src/context/contextual_user_message_tests.rs +++ b/codex-rs/core/src/context/contextual_user_message_tests.rs @@ -14,9 +14,10 @@ fn detects_environment_context_fragment() { } #[test] -fn detects_legacy_goal_context_fragment() { - assert!(is_contextual_user_fragment(&ContentItem::InputText { - text: "\nlegacy hidden goal prompt\n".to_string(), +fn does_not_hard_code_extension_context_fragments() { + assert!(!is_contextual_user_fragment(&ContentItem::InputText { + text: "\nextension-owned hidden prompt\n" + .to_string(), })); } diff --git a/codex-rs/core/src/context/goal_context.rs b/codex-rs/core/src/context/goal_context.rs deleted file mode 100644 index fa84b0ed5a..0000000000 --- a/codex-rs/core/src/context/goal_context.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Legacy hidden user-context marker for goal steering prompts. -//! -//! The goal implementation now lives in `codex-goal-extension`, but existing -//! rollouts can still contain this marker. Core keeps recognizing it so old -//! hidden goal prompts do not resurface as normal user messages after resume. - -use super::ContextualUserFragment; - -/// Marker-only registration for hidden goal steering context. -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct GoalContext; - -impl ContextualUserFragment for GoalContext { - fn role() -> &'static str { - "user" - } - - fn markers(&self) -> (&'static str, &'static str) { - Self::type_markers() - } - - fn body(&self) -> String { - String::new() - } - - fn type_markers() -> (&'static str, &'static str) { - ("", "") - } -} diff --git a/codex-rs/core/src/context/mod.rs b/codex-rs/core/src/context/mod.rs index 70b4dda424..5e268e9052 100644 --- a/codex-rs/core/src/context/mod.rs +++ b/codex-rs/core/src/context/mod.rs @@ -9,7 +9,6 @@ mod contextual_user_message; mod environment_context; mod fragment; mod fragments; -mod goal_context; mod guardian_followup_review_reminder; mod hook_additional_context; mod image_generation_instructions; @@ -43,7 +42,6 @@ pub(crate) use fragment::FragmentRegistration; pub(crate) use fragment::FragmentRegistrationProxy; pub(crate) use fragments::AdditionalContextDeveloperFragment; pub(crate) use fragments::AdditionalContextUserFragment; -pub(crate) use goal_context::GoalContext; pub(crate) use guardian_followup_review_reminder::GuardianFollowupReviewReminder; pub(crate) use hook_additional_context::HookAdditionalContext; pub(crate) use image_generation_instructions::ImageGenerationInstructions; diff --git a/codex-rs/core/src/tasks/idle_extension.rs b/codex-rs/core/src/tasks/idle_extension.rs index 785d44dba1..e1483fca73 100644 --- a/codex-rs/core/src/tasks/idle_extension.rs +++ b/codex-rs/core/src/tasks/idle_extension.rs @@ -252,7 +252,7 @@ mod tests { let original_len = prompt.len(); let request = ThreadIdleRequest::new(HiddenContext::new( - HiddenContextMarker::new("", ""), + HiddenContextMarker::new("", ""), prompt, )); let TurnInput::ResponseInputItem(ResponseInputItem::Message { content, .. }) = @@ -264,7 +264,7 @@ mod tests { panic!("expected one text content item"); }; - assert!(text.starts_with("")); + assert!(text.starts_with("")); assert!(text.contains("start")); assert!(text.contains("end")); assert!(text.len() < original_len); diff --git a/codex-rs/ext/extension-api/Cargo.toml b/codex-rs/ext/extension-api/Cargo.toml index 15f6e99fa0..9a76bb74bb 100644 --- a/codex-rs/ext/extension-api/Cargo.toml +++ b/codex-rs/ext/extension-api/Cargo.toml @@ -17,3 +17,4 @@ workspace = true async-trait = { workspace = true } codex-protocol = { workspace = true } codex-tools = { workspace = true } +inventory = { workspace = true } diff --git a/codex-rs/ext/extension-api/src/context.rs b/codex-rs/ext/extension-api/src/context.rs index 88dd5004f2..6324e7583e 100644 --- a/codex-rs/ext/extension-api/src/context.rs +++ b/codex-rs/ext/extension-api/src/context.rs @@ -18,6 +18,40 @@ impl HiddenContextMarker { end_marker, } } + + /// Returns true when text is wrapped in this marker pair. + pub fn matches_text(self, text: &str) -> bool { + 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()) + .is_some_and(|candidate| candidate.eq_ignore_ascii_case(self.start_marker)); + let trimmed = trimmed.trim_end(); + let ends_with_marker = trimmed + .get(trimmed.len().saturating_sub(self.end_marker.len())..) + .is_some_and(|candidate| candidate.eq_ignore_ascii_case(self.end_marker)); + starts_with_marker && ends_with_marker + } +} + +/// Compile-time registration for extension-owned hidden context markers. +/// +/// Extensions use this to reserve their hidden context wire tags without adding +/// feature-specific tags to core parsing code. +pub struct HiddenContextMarkerRegistration { + pub marker: HiddenContextMarker, +} + +inventory::collect!(HiddenContextMarkerRegistration); + +/// Returns all hidden context markers registered by linked extensions. +pub fn registered_hidden_context_markers() -> impl Iterator { + inventory::iter:: + .into_iter() + .map(|registration| registration.marker) } /// Extension-owned hidden context body with the marker pair used to wrap it. diff --git a/codex-rs/ext/extension-api/src/lib.rs b/codex-rs/ext/extension-api/src/lib.rs index a02ab376d3..218fd91bbb 100644 --- a/codex-rs/ext/extension-api/src/lib.rs +++ b/codex-rs/ext/extension-api/src/lib.rs @@ -27,6 +27,8 @@ pub use codex_tools::parse_tool_input_schema; pub use codex_tools::parse_tool_input_schema_without_compaction; pub use context::HiddenContext; pub use context::HiddenContextMarker; +pub use context::HiddenContextMarkerRegistration; +pub use context::registered_hidden_context_markers; pub use contributors::ApprovalReviewContributor; pub use contributors::ConfigContributor; pub use contributors::ContextContributor; diff --git a/codex-rs/ext/goal/Cargo.toml b/codex-rs/ext/goal/Cargo.toml index 0138d28ade..13dd63dd12 100644 --- a/codex-rs/ext/goal/Cargo.toml +++ b/codex-rs/ext/goal/Cargo.toml @@ -21,6 +21,7 @@ codex-protocol = { workspace = true } codex-state = { workspace = true } codex-tools = { workspace = true } codex-utils-template = { workspace = true } +inventory = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tokio = { workspace = true, features = ["sync"] } diff --git a/codex-rs/ext/goal/src/steering.rs b/codex-rs/ext/goal/src/steering.rs index 480ca7090d..5c1bd0d581 100644 --- a/codex-rs/ext/goal/src/steering.rs +++ b/codex-rs/ext/goal/src/steering.rs @@ -1,5 +1,6 @@ use codex_extension_api::HiddenContext; use codex_extension_api::HiddenContextMarker; +use codex_extension_api::HiddenContextMarkerRegistration; use codex_extension_api::ThreadIdleRequest; use codex_protocol::protocol::ThreadGoal; use codex_utils_template::Template; @@ -8,6 +9,12 @@ use std::sync::LazyLock; const GOAL_CONTEXT_MARKER: HiddenContextMarker = HiddenContextMarker::new("", ""); +inventory::submit! { + HiddenContextMarkerRegistration { + marker: GOAL_CONTEXT_MARKER, + } +} + static CONTINUATION_TEMPLATE: LazyLock