Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 4 additions & 15 deletions codex-rs/app-server/src/request_processors/catalog_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
78 changes: 31 additions & 47 deletions codex-rs/app-server/tests/suite/v2/skills_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"),
Expand Down Expand Up @@ -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(),
Expand All @@ -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?;
Expand All @@ -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(())
}
Expand Down
8 changes: 8 additions & 0 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
80 changes: 80 additions & 0 deletions codex-rs/tui/src/app_server_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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>,
}

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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");
Expand Down
Loading