Isolate automatic plugin Git operations (#39520)

## Why

Background marketplace and plugin refreshes can otherwise inherit repository-local or command-scoped Git configuration from the project that launched Codex. That configuration can redirect remotes or invoke Git helpers during an automatic operation.

## What changed

- Run automatic marketplace upgrades, plugin cache refreshes, and curated marketplace lookups with repository-scoped Git environment variables removed and a temporary trusted repository under the Codex home.
- Carry the automatic/manual trust mode through marketplace upgrades and plugin materialization.
- Preserve the caller's Git configuration for explicit marketplace upgrades, plugin installs, and refreshes.

## Testing

- Add regression coverage for repository URL rewrites, protocol helpers, command-scoped configuration, and manual Git filters.

GitOrigin-RevId: 957f45fa3bd7ce7e1b83355f276cf65dc5e99c8e
This commit is contained in:
Charlie Marsh authored and copyberry committed 2026-08-19 17:34:00 +00:00
1 parent 18937b2265
commit ffad922340
13 files changed
+690 -89

No files matched your search

+60 -20
View File
@@ -1,3 +1,4 @@
use crate::PluginGitMode;
use crate::app_mcp_routing::apply_app_mcp_routing_policy;
use crate::app_mcp_routing::apps_route_available;
use crate::is_openai_curated_marketplace_name;
@@ -56,7 +57,6 @@ use std::collections::HashMap;
use std::collections::HashSet;
use std::fs;
use std::path::Path;
use std::process::Command;
use tempfile::TempDir;
use tracing::instrument;
use tracing::warn;
@@ -496,6 +496,7 @@ pub(crate) fn refresh_non_curated_plugin_cache(
codex_home,
additional_roots,
configured_plugin_keys,
PluginGitMode::Automatic,
))
}
@@ -503,12 +504,14 @@ pub(crate) fn refresh_non_curated_plugin_cache_detailed(
codex_home: &Path,
additional_roots: &[AbsolutePathBuf],
configured_plugin_keys: &[String],
git_mode: PluginGitMode,
) -> Result<NonCuratedCacheRefreshOutcome, String> {
refresh_non_curated_plugin_cache_with_mode(
codex_home,
additional_roots,
configured_plugin_keys,
NonCuratedCacheRefreshMode::IfVersionChanged,
git_mode,
)
}
@@ -522,6 +525,7 @@ pub(crate) fn refresh_non_curated_plugin_cache_force_reinstall(
codex_home,
additional_roots,
configured_plugin_keys,
PluginGitMode::Automatic,
))
}
@@ -529,12 +533,14 @@ pub(crate) fn refresh_non_curated_plugin_cache_force_reinstall_detailed(
codex_home: &Path,
additional_roots: &[AbsolutePathBuf],
configured_plugin_keys: &[String],
git_mode: PluginGitMode,
) -> Result<NonCuratedCacheRefreshOutcome, String> {
refresh_non_curated_plugin_cache_with_mode(
codex_home,
additional_roots,
configured_plugin_keys,
NonCuratedCacheRefreshMode::ForceReinstall,
git_mode,
)
}
@@ -543,6 +549,7 @@ fn refresh_non_curated_plugin_cache_with_mode(
additional_roots: &[AbsolutePathBuf],
configured_plugin_keys: &[String],
mode: NonCuratedCacheRefreshMode,
git_mode: PluginGitMode,
) -> Result<NonCuratedCacheRefreshOutcome, String> {
let mut configured_non_curated_plugin_ids = configured_plugin_keys
.iter()
@@ -644,9 +651,10 @@ fn refresh_non_curated_plugin_cache_with_mode(
};
let refresh_result = (|| -> Result<bool, String> {
let materialized =
materialize_marketplace_plugin_source(codex_home, &source).map_err(|err| {
format!("failed to materialize plugin source for {plugin_key}: {err}")
})?;
materialize_marketplace_plugin_source_with_mode(codex_home, &source, git_mode)
.map_err(|err| {
format!("failed to materialize plugin source for {plugin_key}: {err}")
})?;
let source_path = materialized.path;
let plugin_version = match manifest_fallback_contents.as_deref() {
Some(manifest_contents) => plugin_version_for_source_with_fallback_manifest(
@@ -1706,6 +1714,15 @@ pub struct MaterializedMarketplacePluginSource {
pub fn materialize_marketplace_plugin_source(
codex_home: &Path,
source: &MarketplacePluginSource,
) -> Result<MaterializedMarketplacePluginSource, String> {
materialize_marketplace_plugin_source_with_mode(codex_home, source, PluginGitMode::Manual)
}
/// Applies the initiating operation's Git trust policy throughout plugin materialization.
pub(crate) fn materialize_marketplace_plugin_source_with_mode(
codex_home: &Path,
source: &MarketplacePluginSource,
mode: PluginGitMode,
) -> Result<MaterializedMarketplacePluginSource, String> {
match source {
MarketplacePluginSource::Local { path } => Ok(MaterializedMarketplacePluginSource {
@@ -1735,11 +1752,13 @@ pub fn materialize_marketplace_plugin_source(
)
})?;
clone_git_plugin_source(
codex_home,
url,
ref_name.as_deref(),
sha.as_deref(),
path.as_deref(),
tempdir.path(),
mode,
)?;
let path = if let Some(path) = path {
AbsolutePathBuf::try_from(tempdir.path().join(path)).map_err(|err| {
@@ -1775,12 +1794,18 @@ pub fn materialize_marketplace_plugin_source(
}
fn clone_git_plugin_source(
codex_home: &Path,
url: &str,
ref_name: Option<&str>,
sha: Option<&str>,
sparse_checkout_path: Option<&str>,
destination: &Path,
mode: PluginGitMode,
) -> Result<(), String> {
let clone_cwd = match mode {
PluginGitMode::Automatic => Some(codex_home),
PluginGitMode::Manual => None,
};
if let Some(sparse_checkout_path) = sparse_checkout_path {
run_git(
&[
@@ -1791,7 +1816,8 @@ fn clone_git_plugin_source(
url,
destination.to_string_lossy().as_ref(),
],
/*cwd*/ None,
clone_cwd,
mode,
)?;
run_git(
&[
@@ -1802,42 +1828,56 @@ fn clone_git_plugin_source(
sparse_checkout_path,
],
Some(destination),
mode,
)?;
} else {
run_git(
&["clone", url, destination.to_string_lossy().as_ref()],
/*cwd*/ None,
clone_cwd,
mode,
)?;
}
if let Some(sha) = sha {
run_git(&["checkout", sha], Some(destination))?;
let checked_out_sha = run_git_output(&["rev-parse", "HEAD"], Some(destination))?;
run_git(&["checkout", sha], Some(destination), mode)?;
let checked_out_sha = run_git_output(&["rev-parse", "HEAD"], Some(destination), mode)?;
if !checked_out_sha.eq_ignore_ascii_case(sha) {
return Err(format!(
"checked out Git SHA {checked_out_sha} does not match requested SHA {sha}"
));
}
} else if let Some(ref_name) = ref_name {
run_git(&["checkout", ref_name], Some(destination))?;
run_git(&["checkout", ref_name], Some(destination), mode)?;
} else if sparse_checkout_path.is_some() {
run_git(&["checkout"], Some(destination))?;
run_git(&["checkout"], Some(destination), mode)?;
}
Ok(())
}
fn run_git(args: &[&str], cwd: Option<&Path>) -> Result<(), String> {
run_git_output(args, cwd).map(drop)
fn run_git(args: &[&str], cwd: Option<&Path>, mode: PluginGitMode) -> Result<(), String> {
run_git_output(args, cwd, mode).map(drop)
}
fn run_git_output(args: &[&str], cwd: Option<&Path>) -> Result<String, String> {
let mut command = Command::new("git");
command
.args(["-c", codex_git_utils::SAFE_BARE_REPOSITORY_CONFIG])
.args(args);
fn run_git_output(
args: &[&str],
cwd: Option<&Path>,
mode: PluginGitMode,
) -> Result<String, String> {
let mut command = mode.command(Path::new("git"));
command.args(args);
command.env("GIT_TERMINAL_PROMPT", "0");
if let Some(cwd) = cwd {
command.current_dir(cwd);
}
let _trusted_repository = if let Some(cwd) = cwd
&& args.first() == Some(&"clone")
{
Some(crate::configure_trusted_git_repository(&mut command, cwd)?)
} else {
if let Some(cwd) = cwd {
command.current_dir(cwd);
if matches!(mode, PluginGitMode::Manual) {
command.env_remove("GIT_DIR");
}
}
None
};
let output = command
.output()