[codex] Describe uninstalled cross-repo plugin reads (#18449)

## Summary
- Populate `PluginDetail.description` in core for uninstalled cross-repo
plugins when detailed fields are unavailable until install.
- Include the source Git URL plus optional path/ref/sha details in that
fallback description.
- Keep `details_unavailable_reason` as the structured signal while
app-server forwards the description normally.
- Add plugin-read coverage proving the response does not clone the
remote source just to show the message.

## Why
Uninstalled cross-repo plugins intentionally return sparse detail data
so listing/reading does not clone the plugin source. Without a
description, Desktop and TUI detail pages look like an ordinary empty
plugin. This gives users a concrete explanation and source pointer while
keeping the existing structured reason available for callers.

## Validation
- `just fmt`
- `cargo test -p codex-core
read_plugin_for_config_uninstalled_git_source_requires_install_without_cloning`
- `cargo test -p codex-app-server plugin_read --test all`
- `just fix -p codex-core`
- `just fix -p codex-app-server`

Note: `cargo test -p codex-app-server` was also attempted before the
latest refactor and failed broadly in unrelated v2
thread/realtime/review/skills suites; the new plugin-read test passed in
that run as well.
This commit is contained in:
xli-oai
2026-04-17 20:31:13 -07:00
committed by GitHub
parent 3f7222ec76
commit def6467d2b
3 changed files with 107 additions and 2 deletions
+30 -1
View File
@@ -1058,10 +1058,11 @@ impl PluginsManager {
})?;
let plugin_key = plugin_id.as_key();
if matches!(plugin.source, MarketplacePluginSource::Git { .. }) && !plugin.installed {
let description = remote_plugin_install_required_description(&plugin.source);
return Ok(PluginDetail {
id: plugin_key,
name: plugin.name,
description: None,
description: Some(description),
source: plugin.source,
policy: plugin.policy,
interface: plugin.interface,
@@ -1495,6 +1496,34 @@ impl PluginsManager {
}
}
fn remote_plugin_install_required_description(source: &MarketplacePluginSource) -> String {
let source_description = match source {
MarketplacePluginSource::Git {
url,
path,
ref_name,
sha,
} => {
let mut parts = vec![url.clone()];
if let Some(path) = path {
parts.push(format!("path `{path}`"));
}
if let Some(ref_name) = ref_name {
parts.push(format!("ref `{ref_name}`"));
}
if let Some(sha) = sha {
parts.push(format!("sha `{sha}`"));
}
parts.join(", ")
}
MarketplacePluginSource::Local { path } => path.as_path().display().to_string(),
};
format!(
"This is a cross-repo plugin. Install it to view more detailed information. The source of the plugin is {source_description}."
)
}
#[derive(Debug, thiserror::Error)]
pub enum PluginInstallError {
#[error("{0}")]
+7 -1
View File
@@ -1623,7 +1623,13 @@ plugins = true
Some(PluginDetailsUnavailableReason::InstallRequiredForRemoteSource)
);
assert!(!outcome.plugin.installed);
assert!(outcome.plugin.description.is_none());
let expected_description = format!(
"This is a cross-repo plugin. Install it to view more detailed information. The source of the plugin is {missing_remote_repo_url}, path `plugins/toolkit`."
);
assert_eq!(
outcome.plugin.description.as_deref(),
Some(expected_description.as_str())
);
assert!(outcome.plugin.skills.is_empty());
assert!(outcome.plugin.apps.is_empty());
assert!(outcome.plugin.mcp_server_names.is_empty());