-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Gate Codex skills flag on CLI support #6520
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,10 +39,13 @@ pub struct CodexProvider { | |
| model: ModelConfig, | ||
| #[serde(skip)] | ||
| name: String, | ||
| /// Reasoning effort level (low, medium, high) | ||
| reasoning_effort: String, | ||
| /// Whether to enable skills | ||
| enable_skills: bool, | ||
| // NOTE: Codex CLI removed the `skills` feature flag in openai/codex#8850 and now loads | ||
| // skills unconditionally. To stay compatible with older Codex CLIs (where `skills` was a | ||
| // feature flag), we only pass `--enable/--disable skills` if the installed CLI still | ||
| // advertises that flag via `codex features list`. | ||
| skills_feature_flag_supported: bool, | ||
| /// Whether to skip git repo check | ||
| skip_git_check: bool, | ||
| } | ||
|
|
@@ -70,12 +73,16 @@ impl CodexProvider { | |
| "high".to_string() | ||
| }; | ||
|
|
||
| // Get enable_skills from config, default to true | ||
| let enable_skills = config | ||
| .get_codex_enable_skills() | ||
| .map(|s| s.to_lowercase() == "true") | ||
| .unwrap_or(true); | ||
|
|
||
| let skills_feature_flag_supported = | ||
| Self::supports_feature_flag(&resolved_command, "skills") | ||
| .await | ||
| .unwrap_or(false); | ||
|
|
||
| // Get skip_git_check from config, default to false | ||
| let skip_git_check = config | ||
| .get_codex_skip_git_check() | ||
|
|
@@ -88,10 +95,66 @@ impl CodexProvider { | |
| name: Self::metadata().name, | ||
| reasoning_effort, | ||
| enable_skills, | ||
| skills_feature_flag_supported, | ||
| skip_git_check, | ||
| }) | ||
| } | ||
|
|
||
| fn feature_list_contains_feature(stdout: &str, feature: &str) -> bool { | ||
| // `codex features list` output is a whitespace-separated sequence of: | ||
| // `<name> <stability> <enabled>` repeated, e.g.: | ||
| // `undo stable false shell_tool stable true ...` | ||
| // | ||
| // We match by token to be robust to both newline- and space-delimited output. | ||
| stdout | ||
| .split_whitespace() | ||
| .collect::<Vec<_>>() | ||
| .windows(3) | ||
| .any(|w| { | ||
| w[0] == feature | ||
| && matches!(w[1], "stable" | "beta" | "experimental") | ||
| && matches!(w[2], "true" | "false") | ||
| }) | ||
| } | ||
|
Comment on lines
+103
to
+118
|
||
|
|
||
| fn skills_feature_flag_args( | ||
| skills_feature_flag_supported: bool, | ||
| enable_skills: bool, | ||
| ) -> Option<(&'static str, &'static str)> { | ||
| if !skills_feature_flag_supported { | ||
| return None; | ||
| } | ||
|
|
||
| Some(( | ||
| if enable_skills { | ||
| "--enable" | ||
| } else { | ||
| "--disable" | ||
| }, | ||
| "skills", | ||
| )) | ||
| } | ||
|
|
||
| async fn supports_feature_flag(command: &PathBuf, feature: &str) -> Result<bool> { | ||
| let mut cmd = Command::new(command); | ||
| configure_command_no_window(&mut cmd); | ||
| cmd.arg("features") | ||
| .arg("list") | ||
| .stdout(Stdio::piped()) | ||
| .stderr(Stdio::piped()); | ||
|
|
||
| let output = cmd.output().await?; | ||
| if !output.status.success() { | ||
| return Ok(false); | ||
| } | ||
|
|
||
| let stdout = String::from_utf8_lossy(&output.stdout); | ||
| Ok(Self::feature_list_contains_feature( | ||
| stdout.as_ref(), | ||
| feature, | ||
| )) | ||
| } | ||
|
|
||
| /// Convert goose messages to a simple text prompt format | ||
| /// Similar to Gemini CLI, we use Human:/Assistant: prefixes | ||
| fn messages_to_prompt(&self, system: &str, messages: &[Message]) -> String { | ||
|
|
@@ -165,6 +228,10 @@ impl CodexProvider { | |
| println!("Model: {}", self.model.model_name); | ||
| println!("Reasoning effort: {}", self.reasoning_effort); | ||
| println!("Enable skills: {}", self.enable_skills); | ||
| println!( | ||
| "Skills feature flag supported: {}", | ||
| self.skills_feature_flag_supported | ||
| ); | ||
| println!("Skip git check: {}", self.skip_git_check); | ||
| println!("Prompt length: {} chars", prompt.len()); | ||
| println!("Prompt: {}", prompt); | ||
|
|
@@ -189,9 +256,10 @@ impl CodexProvider { | |
| self.reasoning_effort | ||
| )); | ||
|
|
||
| // Enable skills if configured | ||
| if self.enable_skills { | ||
| cmd.arg("--enable").arg("skills"); | ||
| if let Some((flag, feature)) = | ||
| Self::skills_feature_flag_args(self.skills_feature_flag_supported, self.enable_skills) | ||
| { | ||
| cmd.arg(flag).arg(feature); | ||
| } | ||
|
|
||
| // JSON output format for structured parsing | ||
|
|
@@ -527,6 +595,7 @@ impl Provider for CodexProvider { | |
| "model": model_config.model_name, | ||
| "reasoning_effort": self.reasoning_effort, | ||
| "enable_skills": self.enable_skills, | ||
| "skills_feature_flag_supported": self.skills_feature_flag_supported, | ||
| "system_length": system.len(), | ||
| "messages_count": messages.len() | ||
| }); | ||
|
|
@@ -576,6 +645,7 @@ mod tests { | |
| name: "codex".to_string(), | ||
| reasoning_effort: "high".to_string(), | ||
| enable_skills: true, | ||
| skills_feature_flag_supported: false, | ||
| skip_git_check: false, | ||
| }; | ||
|
|
||
|
|
@@ -591,6 +661,7 @@ mod tests { | |
| name: "codex".to_string(), | ||
| reasoning_effort: "high".to_string(), | ||
| enable_skills: true, | ||
| skills_feature_flag_supported: false, | ||
| skip_git_check: false, | ||
| }; | ||
|
|
||
|
|
@@ -607,6 +678,7 @@ mod tests { | |
| name: "codex".to_string(), | ||
| reasoning_effort: "high".to_string(), | ||
| enable_skills: true, | ||
| skills_feature_flag_supported: false, | ||
| skip_git_check: false, | ||
| }; | ||
|
|
||
|
|
@@ -636,6 +708,7 @@ mod tests { | |
| name: "codex".to_string(), | ||
| reasoning_effort: "high".to_string(), | ||
| enable_skills: true, | ||
| skills_feature_flag_supported: false, | ||
| skip_git_check: false, | ||
| }; | ||
|
|
||
|
|
@@ -656,6 +729,7 @@ mod tests { | |
| name: "codex".to_string(), | ||
| reasoning_effort: "high".to_string(), | ||
| enable_skills: true, | ||
| skills_feature_flag_supported: false, | ||
| skip_git_check: false, | ||
| }; | ||
|
|
||
|
|
@@ -689,6 +763,7 @@ mod tests { | |
| name: "codex".to_string(), | ||
| reasoning_effort: "high".to_string(), | ||
| enable_skills: true, | ||
| skills_feature_flag_supported: false, | ||
| skip_git_check: false, | ||
| }; | ||
|
|
||
|
|
@@ -721,6 +796,7 @@ mod tests { | |
| name: "codex".to_string(), | ||
| reasoning_effort: "high".to_string(), | ||
| enable_skills: true, | ||
| skills_feature_flag_supported: false, | ||
| skip_git_check: false, | ||
| }; | ||
|
|
||
|
|
@@ -746,6 +822,7 @@ mod tests { | |
| name: "codex".to_string(), | ||
| reasoning_effort: "high".to_string(), | ||
| enable_skills: true, | ||
| skills_feature_flag_supported: false, | ||
| skip_git_check: false, | ||
| }; | ||
|
|
||
|
|
@@ -770,6 +847,7 @@ mod tests { | |
| name: "codex".to_string(), | ||
| reasoning_effort: "high".to_string(), | ||
| enable_skills: true, | ||
| skills_feature_flag_supported: false, | ||
| skip_git_check: false, | ||
| }; | ||
|
|
||
|
|
@@ -792,6 +870,7 @@ mod tests { | |
| name: "codex".to_string(), | ||
| reasoning_effort: "high".to_string(), | ||
| enable_skills: true, | ||
| skills_feature_flag_supported: false, | ||
| skip_git_check: false, | ||
| }; | ||
|
|
||
|
|
@@ -819,6 +898,7 @@ mod tests { | |
| name: "codex".to_string(), | ||
| reasoning_effort: "high".to_string(), | ||
| enable_skills: true, | ||
| skills_feature_flag_supported: false, | ||
| skip_git_check: false, | ||
| }; | ||
|
|
||
|
|
@@ -851,6 +931,7 @@ mod tests { | |
| name: "codex".to_string(), | ||
| reasoning_effort: "high".to_string(), | ||
| enable_skills: true, | ||
| skills_feature_flag_supported: false, | ||
| skip_git_check: false, | ||
| }; | ||
|
|
||
|
|
@@ -898,6 +979,7 @@ mod tests { | |
| name: "codex".to_string(), | ||
| reasoning_effort: "high".to_string(), | ||
| enable_skills: true, | ||
| skills_feature_flag_supported: false, | ||
| skip_git_check: false, | ||
| }; | ||
|
|
||
|
|
@@ -924,6 +1006,7 @@ mod tests { | |
| name: "codex".to_string(), | ||
| reasoning_effort: "high".to_string(), | ||
| enable_skills: true, | ||
| skills_feature_flag_supported: false, | ||
| skip_git_check: false, | ||
| }; | ||
|
|
||
|
|
@@ -952,4 +1035,32 @@ mod tests { | |
| fn test_default_model() { | ||
| assert_eq!(CODEX_DEFAULT_MODEL, "gpt-5.2-codex"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_skills_feature_flag_args_based_on_features_list_support() { | ||
| let stdout = "undo stable false shell_tool stable true skills stable true steer beta false"; | ||
| let skills_supported = CodexProvider::feature_list_contains_feature(stdout, "skills"); | ||
| assert!(skills_supported); | ||
|
|
||
| assert_eq!( | ||
| CodexProvider::skills_feature_flag_args(skills_supported, true), | ||
| Some(("--enable", "skills")) | ||
| ); | ||
| assert_eq!( | ||
| CodexProvider::skills_feature_flag_args(skills_supported, false), | ||
| Some(("--disable", "skills")) | ||
| ); | ||
|
|
||
| let stdout = "undo stable false shell_tool stable true steer beta false"; | ||
| let skills_supported = CodexProvider::feature_list_contains_feature(stdout, "skills"); | ||
| assert!(!skills_supported); | ||
| assert_eq!( | ||
| CodexProvider::skills_feature_flag_args(skills_supported, true), | ||
| None | ||
| ); | ||
| assert_eq!( | ||
| CodexProvider::skills_feature_flag_args(skills_supported, false), | ||
| None | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If stdout has fewer than 3 tokens after
split_whitespace(), thewindows(3)iterator will be empty (not panic), but this edge case should be tested. An empty or malformed output fromcodex features listwould silently returnfalse, which is correct behavior but should be explicitly verified in tests.