Compare commits

...

2 Commits

Author SHA1 Message Date
Alex Zamoshchin
052c92d2ac format catalog skill root loading 2026-05-14 13:07:52 -04:00
Alex Zamoshchin
b17627f531 preserve skills when workspace plugins are disabled 2026-05-14 12:12:49 -04:00
4 changed files with 123 additions and 62 deletions

View File

@@ -390,10 +390,6 @@ impl CatalogRequestProcessor {
};
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
let auth = self.auth_manager.auth().await;
let workspace_codex_plugins_enabled = self
.workspace_codex_plugins_enabled(&config, auth.as_ref())
.await;
let skills_manager = self.thread_manager.skills_manager();
let plugins_manager = self.thread_manager.plugins_manager();
let fs = self
@@ -425,17 +421,10 @@ impl CatalogRequestProcessor {
);
}
};
let effective_skill_roots = if workspace_codex_plugins_enabled {
let plugins_input = config.plugins_config_input();
plugins_manager
.effective_skill_roots_for_layer_stack(
&config_layer_stack,
&plugins_input,
)
.await
} else {
Vec::new()
};
let plugins_input = config.plugins_config_input();
let effective_skill_roots = plugins_manager
.effective_skill_roots_for_layer_stack(&config_layer_stack, &plugins_input)
.await;
let skills_input = codex_core::skills::SkillsLoadInput::new(
cwd_abs.clone(),
effective_skill_roots,

View File

@@ -40,22 +40,6 @@ fn write_skill(root: &TempDir, name: &str) -> Result<()> {
Ok(())
}
fn write_plugins_enabled_config_with_base_url(
codex_home: &std::path::Path,
base_url: &str,
) -> std::io::Result<()> {
std::fs::write(
codex_home.join("config.toml"),
format!(
r#"chatgpt_base_url = "{base_url}"
[features]
plugins = true
"#,
),
)
}
fn write_remote_plugins_enabled_config_with_base_url(
codex_home: &std::path::Path,
base_url: &str,
@@ -73,32 +57,17 @@ remote_plugin = true
)
}
fn write_plugin_with_skill(
repo_root: &std::path::Path,
fn write_cached_local_plugin_with_skill(
codex_home: &std::path::Path,
marketplace_name: &str,
plugin_name: &str,
skill_name: &str,
) -> Result<()> {
std::fs::create_dir_all(repo_root.join(".git"))?;
std::fs::create_dir_all(repo_root.join(".agents/plugins"))?;
std::fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
format!(
r#"{{
"name": "local-marketplace",
"plugins": [
{{
"name": "{plugin_name}",
"source": {{
"source": "local",
"path": "./{plugin_name}"
}}
}}
]
}}"#
),
)?;
let plugin_root = repo_root.join(plugin_name);
let plugin_root = codex_home
.join("plugins/cache")
.join(marketplace_name)
.join(plugin_name)
.join("local");
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
std::fs::write(
plugin_root.join(".codex-plugin/plugin.json"),
@@ -316,15 +285,30 @@ async fn skills_list_loads_remote_installed_plugin_skills_from_cache() -> Result
}
#[tokio::test]
async fn skills_list_excludes_plugin_skills_when_workspace_codex_plugins_disabled() -> Result<()> {
async fn skills_list_keeps_plugin_skills_when_workspace_codex_plugins_disabled() -> Result<()> {
let codex_home = TempDir::new()?;
let repo_root = TempDir::new()?;
let cwd = TempDir::new()?;
let server = MockServer::start().await;
write_skill(&codex_home, "home-skill")?;
write_plugin_with_skill(repo_root.path(), "demo-plugin", "plugin-skill")?;
write_plugins_enabled_config_with_base_url(
write_cached_local_plugin_with_skill(
codex_home.path(),
&format!("{}/backend-api/", server.uri()),
"debug",
"demo-plugin",
"plugin-skill",
)?;
std::fs::write(
codex_home.path().join("config.toml"),
format!(
r#"chatgpt_base_url = "{}/backend-api/"
[features]
plugins = true
[plugins."demo-plugin@debug"]
enabled = true
"#,
server.uri()
),
)?;
write_chatgpt_auth(
codex_home.path(),
@@ -351,7 +335,7 @@ async fn skills_list_excludes_plugin_skills_when_workspace_codex_plugins_disable
let request_id = mcp
.send_skills_list_request(SkillsListParams {
cwds: vec![repo_root.path().to_path_buf()],
cwds: vec![cwd.path().to_path_buf()],
force_reload: true,
})
.await?;
@@ -374,8 +358,8 @@ async fn skills_list_excludes_plugin_skills_when_workspace_codex_plugins_disable
data[0]
.skills
.iter()
.all(|skill| skill.name != "demo-plugin:plugin-skill"),
"plugin skills should be hidden when workspace Codex plugins are disabled"
.any(|skill| skill.name == "demo-plugin:plugin-skill"),
"plugin-backed skills should remain available when workspace Codex plugins are disabled"
);
Ok(())
}

View File

@@ -690,6 +690,14 @@ impl App {
}
};
let bootstrap = app_server.bootstrap(&config).await?;
if !bootstrap.plugins_enabled
&& let Err(err) = config.features.disable(Feature::Plugins)
{
tracing::warn!(
error = %err,
"failed to apply app-server plugin visibility to the TUI config"
);
}
let mut model = bootstrap.default_model;
let available_models = bootstrap.available_models;
let exit_info = handle_model_migration_prompt_if_needed(

View File

@@ -20,6 +20,8 @@ use codex_app_server_protocol::AuthMode;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::ConfigBatchWriteParams;
use codex_app_server_protocol::ConfigWriteResponse;
use codex_app_server_protocol::ExperimentalFeatureListParams;
use codex_app_server_protocol::ExperimentalFeatureListResponse;
use codex_app_server_protocol::ExternalAgentConfigDetectParams;
use codex_app_server_protocol::ExternalAgentConfigDetectResponse;
use codex_app_server_protocol::ExternalAgentConfigImportParams;
@@ -104,6 +106,7 @@ use codex_app_server_protocol::TurnStartResponse;
use codex_app_server_protocol::TurnSteerParams;
use codex_app_server_protocol::TurnSteerResponse;
use codex_app_server_protocol::UserInput;
use codex_features::Feature;
use codex_otel::TelemetryAuthMode;
use codex_protocol::ThreadId;
use codex_protocol::approvals::GuardianAssessmentEvent;
@@ -145,6 +148,7 @@ pub(crate) struct AppServerBootstrap {
pub(crate) default_model: String,
pub(crate) feedback_audience: FeedbackAudience,
pub(crate) has_chatgpt_account: bool,
pub(crate) plugins_enabled: bool,
pub(crate) available_models: Vec<ModelPreset>,
}
@@ -218,6 +222,7 @@ impl AppServerSession {
.into_iter()
.map(model_preset_from_api_model)
.collect::<Vec<_>>();
let plugins_enabled = self.plugins_enabled_for_cli(config).await;
let default_model = config
.model
.clone()
@@ -278,10 +283,32 @@ impl AppServerSession {
default_model,
feedback_audience,
has_chatgpt_account,
plugins_enabled,
available_models,
})
}
async fn plugins_enabled_for_cli(&mut self, config: &Config) -> bool {
let request_id = self.next_request_id();
match self
.client
.request_typed(ClientRequest::ExperimentalFeatureList {
request_id,
params: ExperimentalFeatureListParams::default(),
})
.await
{
Ok(response) => plugins_enabled_from_feature_list(config, &response),
Err(err) => {
tracing::warn!(
error = %err,
"experimentalFeature/list failed during TUI bootstrap; keeping local plugin feature state"
);
config.features.enabled(Feature::Plugins)
}
}
}
/// Fetches the current account info without refreshing the auth token.
///
/// Used by both `bootstrap` (to populate the initial UI) and `get_login_status`
@@ -1022,6 +1049,20 @@ fn thread_realtime_start_params(
.wrap_err("mapping TUI realtime start params to app-server params")
}
fn plugins_enabled_from_feature_list(
config: &Config,
response: &ExperimentalFeatureListResponse,
) -> bool {
response
.data
.iter()
.find(|feature| feature.name == Feature::Plugins.key())
.map_or_else(
|| config.features.enabled(Feature::Plugins),
|feature| feature.enabled,
)
}
pub(crate) fn status_account_display_from_auth_mode(
auth_mode: Option<AuthMode>,
plan_type: Option<codex_protocol::account::PlanType>,
@@ -1608,6 +1649,45 @@ mod tests {
.expect("config should build")
}
#[tokio::test]
async fn plugins_enabled_from_feature_list_uses_app_server_effective_state() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let config = build_config(&temp_dir).await;
assert!(config.features.enabled(Feature::Plugins));
let response = ExperimentalFeatureListResponse {
data: vec![codex_app_server_protocol::ExperimentalFeature {
name: Feature::Plugins.key().to_string(),
stage: codex_app_server_protocol::ExperimentalFeatureStage::Stable,
display_name: None,
description: None,
announcement: None,
enabled: false,
default_enabled: true,
}],
next_cursor: None,
};
assert!(!plugins_enabled_from_feature_list(&config, &response));
}
#[tokio::test]
async fn plugins_enabled_from_feature_list_falls_back_to_local_feature_state() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let mut config = build_config(&temp_dir).await;
config
.features
.disable(Feature::Plugins)
.expect("plugins feature should be mutable in tests");
let response = ExperimentalFeatureListResponse {
data: Vec::new(),
next_cursor: None,
};
assert!(!plugins_enabled_from_feature_list(&config, &response));
}
#[tokio::test]
async fn thread_start_params_include_cwd_for_embedded_sessions() {
let temp_dir = tempfile::tempdir().expect("tempdir");