1- use std:: collections:: HashMap ;
21use std:: collections:: HashSet ;
32use std:: sync:: Arc ;
43
54use crate :: SkillLoadOutcome ;
65use crate :: SkillMetadata ;
7- use crate :: build_skill_name_counts;
86use codex_analytics:: AnalyticsEventsClient ;
97use codex_analytics:: InvocationType ;
108use codex_analytics:: SkillInvocation ;
119use codex_analytics:: TrackEventsContext ;
1210use codex_exec_server:: LOCAL_FS ;
1311use codex_otel:: SessionTelemetry ;
1412use codex_otel:: sanitize_metric_tag_value;
15- use codex_protocol:: user_input:: UserInput ;
1613pub use codex_skills:: ToolMentionKind ;
1714pub use codex_skills:: ToolMentions ;
1815pub use codex_skills:: app_id_from_path;
@@ -21,7 +18,6 @@ pub use codex_skills::extract_tool_mentions_with_sigil;
2118pub use codex_skills:: normalize_skill_path;
2219pub use codex_skills:: plugin_config_name_from_path;
2320pub use codex_skills:: tool_kind_for_path;
24- use codex_utils_absolute_path:: AbsolutePathBuf ;
2521use codex_utils_path_uri:: PathUri ;
2622use codex_utils_string:: take_bytes_at_char_boundary;
2723
@@ -177,169 +173,6 @@ fn emit_skill_injected_metric(
177173 ) ;
178174}
179175
180- /// Collect explicitly mentioned skills from structured and text mentions.
181- ///
182- /// Structured `UserInput::Skill` selections are resolved first by path against
183- /// enabled skills. Text inputs are then scanned to extract `$skill-name` tokens, and we
184- /// iterate loaded skills in their existing order to preserve prior ordering semantics.
185- /// Explicit paths match either a skill's canonical identity or its logical discovery
186- /// path, and plain names are only used when the match is unambiguous.
187- ///
188- /// Complexity: `O(T + (N_s + N_t) * S)` time, `O(S + M)` space, where:
189- /// `S` = number of skills, `T` = total text length, `N_s` = number of structured skill inputs,
190- /// `N_t` = number of text inputs, `M` = max mentions parsed from a single text input.
191- pub fn collect_explicit_skill_mentions (
192- inputs : & [ UserInput ] ,
193- loaded_skills : & SkillLoadOutcome ,
194- connector_slug_counts : & HashMap < String , usize > ,
195- ) -> Vec < SkillMetadata > {
196- let skill_name_counts =
197- build_skill_name_counts ( & loaded_skills. skills , & loaded_skills. disabled_paths ) . 0 ;
198-
199- let selection_context = SkillSelectionContext {
200- loaded_skills,
201- skill_name_counts : & skill_name_counts,
202- connector_slug_counts,
203- } ;
204- let mut selected: Vec < SkillMetadata > = Vec :: new ( ) ;
205- let mut seen_names: HashSet < String > = HashSet :: new ( ) ;
206- let mut seen_paths: HashSet < AbsolutePathBuf > = HashSet :: new ( ) ;
207- let mut blocked_plain_names: HashSet < String > = HashSet :: new ( ) ;
208-
209- for input in inputs {
210- if let UserInput :: Skill { name, path, .. } = input {
211- blocked_plain_names. insert ( name. clone ( ) ) ;
212- let Ok ( path) = AbsolutePathBuf :: relative_to_current_dir ( path) else {
213- continue ;
214- } ;
215-
216- let Some ( skill) = selection_context. loaded_skills . skills . iter ( ) . find ( |skill| {
217- skill. path_to_skills_md == path
218- || selection_context
219- . loaded_skills
220- . skill_discovery_path_for_path ( & skill. path_to_skills_md )
221- . is_some_and ( |discovery_path| discovery_path == & path)
222- } ) else {
223- continue ;
224- } ;
225-
226- if !selection_context. loaded_skills . is_skill_enabled ( skill)
227- || seen_paths. contains ( & skill. path_to_skills_md )
228- {
229- continue ;
230- }
231-
232- seen_paths. insert ( skill. path_to_skills_md . clone ( ) ) ;
233- seen_names. insert ( skill. name . clone ( ) ) ;
234- selected. push ( skill. clone ( ) ) ;
235- }
236- }
237-
238- for input in inputs {
239- if let UserInput :: Text { text, .. } = input {
240- let mentioned_names = extract_tool_mentions ( text) ;
241- select_skills_from_mentions (
242- & selection_context,
243- & blocked_plain_names,
244- & mentioned_names,
245- & mut seen_names,
246- & mut seen_paths,
247- & mut selected,
248- ) ;
249- }
250- }
251-
252- selected
253- }
254-
255- struct SkillSelectionContext < ' a > {
256- loaded_skills : & ' a SkillLoadOutcome ,
257- skill_name_counts : & ' a HashMap < String , usize > ,
258- connector_slug_counts : & ' a HashMap < String , usize > ,
259- }
260-
261- /// Select mentioned skills while preserving the order of `skills`.
262- fn select_skills_from_mentions (
263- selection_context : & SkillSelectionContext < ' _ > ,
264- blocked_plain_names : & HashSet < String > ,
265- mentions : & ToolMentions < ' _ > ,
266- seen_names : & mut HashSet < String > ,
267- seen_paths : & mut HashSet < AbsolutePathBuf > ,
268- selected : & mut Vec < SkillMetadata > ,
269- ) {
270- if mentions. is_empty ( ) {
271- return ;
272- }
273-
274- let mention_skill_paths: HashSet < String > = mentions
275- . paths ( )
276- . filter ( |path| {
277- !matches ! (
278- tool_kind_for_path( path) ,
279- ToolMentionKind :: App | ToolMentionKind :: Mcp | ToolMentionKind :: Plugin
280- )
281- } )
282- . map ( normalize_host_skill_path)
283- . collect ( ) ;
284-
285- for skill in & selection_context. loaded_skills . skills {
286- if !selection_context. loaded_skills . is_skill_enabled ( skill)
287- || seen_paths. contains ( & skill. path_to_skills_md )
288- {
289- continue ;
290- }
291-
292- let canonical_path = normalize_host_skill_path ( & skill. path_to_skills_md . to_string_lossy ( ) ) ;
293- let matches_discovery_path = selection_context
294- . loaded_skills
295- . skill_discovery_path_for_path ( & skill. path_to_skills_md )
296- . is_some_and ( |discovery_path| {
297- mention_skill_paths. contains ( & normalize_host_skill_path (
298- & discovery_path. to_string_lossy ( ) ,
299- ) )
300- } ) ;
301- if mention_skill_paths. contains ( & canonical_path) || matches_discovery_path {
302- seen_paths. insert ( skill. path_to_skills_md . clone ( ) ) ;
303- seen_names. insert ( skill. name . clone ( ) ) ;
304- selected. push ( skill. clone ( ) ) ;
305- }
306- }
307-
308- for skill in & selection_context. loaded_skills . skills {
309- if !selection_context. loaded_skills . is_skill_enabled ( skill)
310- || seen_paths. contains ( & skill. path_to_skills_md )
311- {
312- continue ;
313- }
314-
315- if blocked_plain_names. contains ( skill. name . as_str ( ) ) {
316- continue ;
317- }
318- if !mentions. contains_plain_name ( skill. name . as_str ( ) ) {
319- continue ;
320- }
321-
322- let skill_count = selection_context
323- . skill_name_counts
324- . get ( skill. name . as_str ( ) )
325- . copied ( )
326- . unwrap_or ( 0 ) ;
327- let connector_count = selection_context
328- . connector_slug_counts
329- . get ( & skill. name . to_ascii_lowercase ( ) )
330- . copied ( )
331- . unwrap_or ( 0 ) ;
332- if skill_count != 1 || connector_count != 0 {
333- continue ;
334- }
335-
336- if seen_names. insert ( skill. name . clone ( ) ) {
337- seen_paths. insert ( skill. path_to_skills_md . clone ( ) ) ;
338- selected. push ( skill. clone ( ) ) ;
339- }
340- }
341- }
342-
343176#[ cfg( test) ]
344- #[ path = "injection_tests .rs" ]
177+ #[ path = "prompt_injection_tests .rs" ]
345178mod tests;
0 commit comments