Files
codex/codex-rs/core-plugins/src/script_attribution.rs
papayo-oai 184ea84526 Add authenticated remote plugin measurement references (#46542)
## What changed

Add `RemotePluginMeasurementCache` to prepare trusted measurement declarations for a single installed `GLOBAL` plugin without loading capabilities or updating the shared plugin catalog.

- Recheck installation authorization on every preparation and scope cached references by authentication identity, endpoint, plugin identity, and version.
- Keep up to four entries, share concurrent downloads, and retain isolated bundle files while commands hold references. Reuse cached historical versions after release updates; download only the current release on a cache miss.
- Respect Windows and POSIX case rules, reject ambiguous matches, and omit authenticated URLs and response bodies from preparation errors.

## Testing

Add bundle preparation tests covering authorization, version reuse, concurrent downloads, cancellation and retry, eviction with live references, case matching, error privacy, and temporary-store cleanup.

GitOrigin-RevId: a68f42b90ff78b7943da676a7b43f1ea76b31582
2026-09-19 00:53:17 +00:00

701 lines
25 KiB
Rust

use crate::OPENAI_API_CURATED_MARKETPLACE_NAME;
use crate::OPENAI_CURATED_MARKETPLACE_NAME;
use crate::OPENAI_PRIMARY_RUNTIME_MARKETPLACE_NAME;
use crate::PluginLoadOutcome;
use crate::loader::curated_plugin_cache_version;
use crate::marketplace::MarketplacePluginSource;
use crate::marketplace::find_marketplace_plugin;
use crate::marketplace_policy::primary_runtime_marketplace_root;
use crate::plugin_metrics::PluginMetricsOperation;
use crate::plugin_metrics::ResolvedPluginMetricsOperation;
use crate::plugin_metrics::load_plugin_metrics_operations;
use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
use crate::startup_sync::curated_plugins_api_marketplace_path;
use crate::startup_sync::curated_plugins_repo_path;
use crate::startup_sync::read_curated_plugins_sha;
use crate::store::DEFAULT_PLUGIN_VERSION;
use crate::store::PluginStore;
use crate::store::plugin_version_for_source;
use crate::store::validate_plugin_version_segment;
use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::GetMetadataOptions;
use codex_exec_server::ReadFileOptions;
use codex_plugin::PluginId;
use codex_protocol::items::is_safe_plugin_relative_path;
use codex_shell_command::bash::extract_bash_command;
use codex_shell_command::bash::parse_shell_lc_plain_commands;
use codex_shell_command::parse_command::is_pathish;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathConvention;
use codex_utils_path_uri::PathUri;
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::path::Component;
use std::path::Path;
#[derive(Clone, Debug, PartialEq, Eq)]
struct TrustedPluginRoot {
plugin_id: PluginId,
version: String,
root: AbsolutePathBuf,
metrics_operations_by_path: BTreeMap<String, PluginMetricsOperation>,
}
/// Trusted plugin command attribution safe to carry into command analytics.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PluginCommandAttribution {
pub plugin_id: PluginId,
pub normalized_relative_path: String,
}
impl PluginCommandAttribution {
/// Returns the paired fields used at command protocol boundaries.
pub fn serialized_fields(&self) -> (String, String) {
(
self.plugin_id.as_key(),
self.normalized_relative_path.clone(),
)
}
}
/// Active first-party roots eligible for command attribution.
/// Trusted means OpenAI-shipped synced or bundled runtime code, or a
/// server-installed global remote plugin cache entry, not a local override.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TrustedPluginRoots {
roots: Vec<TrustedPluginRoot>,
}
/// An untrusted hint from a canonical executor script path, never authorization.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PluginMeasurementTarget {
pub(crate) plugin_id: PluginId,
pub(crate) version: String,
pub(crate) path_convention: PathConvention,
}
impl PluginMeasurementTarget {
/// Selects one remote plugin using the same command parser as attribution.
/// Skip paths outside the remote plugin cache before consulting the executor.
pub async fn from_command(
command: &[String],
cwd: &PathUri,
file_system: &dyn ExecutorFileSystem,
) -> Option<Self> {
let command = single_plain_command(command)?;
let invocation = script_invocation(&command)?;
let script = cwd.join(invocation.script).ok()?;
Self::from_script_path(&script)?;
let script = file_system
.canonicalize(&script, /*sandbox*/ None)
.await
.ok()?;
Self::from_script_path(&script)
}
fn from_script_path(script: &PathUri) -> Option<Self> {
let path_convention = script.infer_path_convention()?;
let native_path = script.inferred_native_path_string();
// Normalize only the hint so equivalent Windows paths share preparation.
// Store identities must retain the authenticated catalog's spelling.
let native_path = match path_convention {
PathConvention::Windows => native_path.to_ascii_lowercase(),
PathConvention::Posix => native_path,
};
let components = path_convention
.path_segments(&native_path)
.collect::<Vec<_>>();
let mut candidates = components.windows(6).filter_map(|parts| {
if parts[..3] != ["plugins", "cache", REMOTE_GLOBAL_MARKETPLACE_NAME]
|| parts[4] == DEFAULT_PLUGIN_VERSION
{
return None;
}
validate_plugin_version_segment(parts[4]).ok()?;
Some(Self {
plugin_id: PluginId::new(parts[3].to_owned(), parts[2].to_owned()).ok()?,
version: parts[4].to_owned(),
path_convention,
})
});
let target = candidates.next()?;
candidates.next().is_none().then_some(target)
}
}
impl TrustedPluginRoots {
/// Called only after the authenticated remote bundle installer has prepared
/// an isolated store. Read declarations without loading plugin capabilities.
pub(crate) fn from_measurement_reference(
codex_home: &Path,
plugin_id: &PluginId,
version: &str,
remote_id: &str,
) -> Option<Self> {
let store = PluginStore::try_new(codex_home.to_owned()).ok()?;
if store.active_plugin_version(plugin_id).as_deref() != Some(version)
|| store.remote_plugin_id(plugin_id).ok()?.as_deref() != Some(remote_id)
{
return None;
}
let root = store.plugin_root(plugin_id, version).canonicalize().ok()?;
let metrics_operations_by_path = load_plugin_metrics_operations(&root).unwrap_or_default();
Some(Self {
roots: vec![TrustedPluginRoot {
plugin_id: plugin_id.clone(),
version: version.to_owned(),
root,
metrics_operations_by_path,
}],
})
}
/// Adds already-trusted roots, preserving the existing root for each plugin and version.
/// Distinct versions remain eligible for exact executor-version matching.
pub fn extend(&mut self, other: &Self) {
let mut seen = self
.roots
.iter()
.map(|root| (root.plugin_id.as_key(), root.version.clone()))
.collect::<HashSet<_>>();
self.roots.extend(
other
.roots
.iter()
.filter(|root| seen.insert((root.plugin_id.as_key(), root.version.clone())))
.cloned(),
);
}
pub fn from_plugin_load_outcome(loaded_plugins: &PluginLoadOutcome, codex_home: &Path) -> Self {
let primary_runtime_marketplace_root = primary_runtime_marketplace_root();
let Ok(store) = PluginStore::try_new(codex_home.to_path_buf()) else {
return Self::default();
};
let mut seen = HashSet::new();
let roots = loaded_plugins
.plugins()
.iter()
.filter(|plugin| plugin.is_active())
.filter_map(|plugin| {
let plugin_id = PluginId::parse(&plugin.config_name).ok()?;
let expected_root = Self::expected_plugin_root(
&store,
codex_home,
&plugin_id,
primary_runtime_marketplace_root.as_deref(),
)?;
if plugin.root != expected_root || !expected_root.as_path().is_dir() {
return None;
}
let version = expected_root.as_path().file_name()?.to_str()?.to_owned();
let root = expected_root.canonicalize().ok()?;
root.as_path().is_dir().then(|| TrustedPluginRoot {
plugin_id,
version,
metrics_operations_by_path: load_plugin_metrics_operations(&root)
.unwrap_or_default(),
root,
})
})
.filter(|root| seen.insert((root.plugin_id.as_key(), root.root.clone())))
.collect();
Self { roots }
}
fn expected_plugin_root(
store: &PluginStore,
codex_home: &Path,
plugin_id: &PluginId,
primary_runtime_marketplace_root: Option<&Path>,
) -> Option<AbsolutePathBuf> {
match plugin_id.marketplace_name.as_str() {
REMOTE_GLOBAL_MARKETPLACE_NAME => {
let active_version = store.active_plugin_version(plugin_id)?;
if active_version == DEFAULT_PLUGIN_VERSION
|| store.remote_plugin_id(plugin_id).ok().flatten().is_none()
{
return None;
}
Some(store.plugin_root(plugin_id, &active_version))
}
OPENAI_CURATED_MARKETPLACE_NAME | OPENAI_API_CURATED_MARKETPLACE_NAME => {
let curated_sha = read_curated_plugins_sha(codex_home)?;
let expected_root =
store.plugin_root(plugin_id, &curated_plugin_cache_version(&curated_sha));
let marketplace_path = match plugin_id.marketplace_name.as_str() {
OPENAI_CURATED_MARKETPLACE_NAME => curated_plugins_repo_path(codex_home)
.join(".agents/plugins/marketplace.json"),
OPENAI_API_CURATED_MARKETPLACE_NAME => {
curated_plugins_api_marketplace_path(codex_home)
}
_ => return None,
};
Self::marketplace_plugin(marketplace_path, plugin_id)?;
Some(expected_root)
}
OPENAI_PRIMARY_RUNTIME_MARKETPLACE_NAME => {
let marketplace_root = primary_runtime_marketplace_root?;
let expected_source_root = AbsolutePathBuf::from_absolute_path_checked(
marketplace_root
.join("plugins")
.join(&plugin_id.plugin_name),
)
.ok()?;
let marketplace_plugin = Self::marketplace_plugin(
marketplace_root.join(".agents/plugins/marketplace.json"),
plugin_id,
)?;
let MarketplacePluginSource::Local { path: source_root } =
marketplace_plugin.source
else {
return None;
};
if source_root != expected_source_root {
return None;
}
let plugin_version = plugin_version_for_source(source_root.as_path()).ok()?;
Some(store.plugin_root(plugin_id, &plugin_version))
}
_ => None,
}
}
fn marketplace_plugin(
marketplace_path: std::path::PathBuf,
plugin_id: &PluginId,
) -> Option<crate::marketplace::ResolvedMarketplacePlugin> {
let marketplace_path =
AbsolutePathBuf::from_absolute_path_checked(marketplace_path).ok()?;
let plugin = find_marketplace_plugin(&marketplace_path, &plugin_id.plugin_name).ok()?;
(plugin.plugin_id == *plugin_id).then_some(plugin)
}
/// Resolves one exact command to one trusted plugin script.
///
/// Complex shell syntax, missing files, symlink escapes, and overlapping
/// matches are all unattributed by design.
pub fn resolve_attribution(
&self,
command: &[String],
cwd: &AbsolutePathBuf,
) -> Option<PluginCommandAttribution> {
self.local_candidate(command, cwd)
.map(|candidate| candidate.attribution())
}
/// Resolves one exact command to one trusted manifest-declared operation.
pub fn resolve_metrics_operation(
&self,
command: &[String],
cwd: &AbsolutePathBuf,
) -> Option<ResolvedPluginMetricsOperation> {
self.local_candidate(command, cwd)?.metrics_operation()
}
fn local_candidate(
&self,
command: &[String],
cwd: &AbsolutePathBuf,
) -> Option<MatchedPluginScript<'_>> {
let command = single_plain_command(command)?;
let invocation = script_invocation(command.as_slice())?;
let script = if Path::new(invocation.script).is_absolute() {
AbsolutePathBuf::from_absolute_path_checked(invocation.script).ok()?
} else {
cwd.join(invocation.script)
}
.canonicalize()
.ok()?;
if !script.as_path().is_file() {
return None;
}
let mut matches = self.roots.iter().filter_map(|root| {
let relative_path = script
.as_path()
.strip_prefix(root.root.as_path())
.ok()
.filter(|relative_path| !relative_path.as_os_str().is_empty())?;
Some(MatchedPluginScript {
root,
normalized_relative_path: normalized_relative_script_path(relative_path)?,
})
});
let attribution = matches.next()?;
matches.next().is_none().then_some(attribution)
}
/// Resolves a trusted script on the selected executor filesystem.
///
/// Remote commands can use a path convention that the app-server host cannot
/// canonicalize. Resolve aliases on that executor first, then match the
/// canonical target to a trusted local plugin script with the same contents.
/// Generic attribution accepts matching script bytes across plugin versions.
pub async fn resolve_executor_attribution(
&self,
command: &[String],
cwd: &PathUri,
file_system: &dyn ExecutorFileSystem,
) -> Option<PluginCommandAttribution> {
self.executor_candidate(command, cwd, file_system, PluginVersionMatch::Any)
.await
.map(|candidate| candidate.matched.attribution())
}
/// Resolves one trusted executor script to one manifest-declared operation,
/// requiring the executor's version to match the trusted declaration's version.
pub async fn resolve_metrics_operation_in_filesystem(
&self,
command: &[String],
cwd: &PathUri,
file_system: &dyn ExecutorFileSystem,
) -> Option<ResolvedPluginMetricsOperation> {
self.executor_candidate(command, cwd, file_system, PluginVersionMatch::Exact)
.await?
.matched
.metrics_operation()
}
async fn executor_candidate(
&self,
command: &[String],
cwd: &PathUri,
file_system: &dyn ExecutorFileSystem,
version_match: PluginVersionMatch,
) -> Option<ExecutorAttributionCandidate<'_>> {
if self.roots.is_empty() {
return None;
}
let command = single_plain_command(command)?;
let invocation = script_invocation(command.as_slice())?;
let script = cwd.join(invocation.script).ok()?;
// Preserve known script suffixes (including directory aliases), but avoid
// an executor round trip for unrelated workspace scripts.
let suffixes = normalized_script_suffixes(&script);
if !self.roots.iter().any(|root| {
suffixes.iter().any(|suffix| {
executor_plugin_root_matches(&script, root, suffix, PluginVersionMatch::Any)
|| root.root.join(suffix).as_path().is_file()
})
}) {
return None;
}
let script = file_system
.canonicalize(&script, /*sandbox*/ None)
.await
.ok()?;
let candidates = self.local_candidates_for_executor_script(&script, version_match);
if candidates.is_empty() {
return None;
}
let metadata = file_system
.get_metadata(
&script,
GetMetadataOptions::default(),
/*sandbox*/ None,
)
.await
.ok()?;
if !metadata.is_file
|| !candidates
.iter()
.any(|candidate| metadata.size == candidate.contents.len() as u64)
{
return None;
}
let contents = file_system
.read_file(&script, ReadFileOptions::default(), /*sandbox*/ None)
.await
.ok()?;
let mut matches = candidates
.into_iter()
.filter(|candidate| contents == candidate.contents);
let candidate = matches.next()?;
match version_match {
PluginVersionMatch::Any => matches
.all(|other| other.matched.attribution() == candidate.matched.attribution())
.then_some(candidate),
PluginVersionMatch::Exact => matches.next().is_none().then_some(candidate),
}
}
fn local_candidates_for_executor_script(
&self,
script: &PathUri,
version_match: PluginVersionMatch,
) -> Vec<ExecutorAttributionCandidate<'_>> {
let suffixes = normalized_script_suffixes(script);
self.roots
.iter()
.filter_map(|root| {
let (script, normalized_relative_path) = suffixes.iter().find_map(|suffix| {
if !executor_plugin_root_matches(script, root, suffix, version_match) {
return None;
}
let script = root.root.join(suffix).canonicalize().ok()?;
let relative_path = script.as_path().strip_prefix(root.root.as_path()).ok()?;
if !script.as_path().is_file() {
return None;
}
let normalized_relative_path = normalized_relative_script_path(relative_path)?;
Some((script, normalized_relative_path))
})?;
Some(ExecutorAttributionCandidate {
matched: MatchedPluginScript {
root,
normalized_relative_path,
},
contents: std::fs::read(script.as_path()).ok()?,
})
})
.collect()
}
}
struct MatchedPluginScript<'a> {
root: &'a TrustedPluginRoot,
normalized_relative_path: String,
}
impl MatchedPluginScript<'_> {
fn attribution(&self) -> PluginCommandAttribution {
PluginCommandAttribution {
plugin_id: self.root.plugin_id.clone(),
normalized_relative_path: self.normalized_relative_path.clone(),
}
}
fn metrics_operation(&self) -> Option<ResolvedPluginMetricsOperation> {
Some(ResolvedPluginMetricsOperation {
plugin_id: self.root.plugin_id.clone(),
operation: self
.root
.metrics_operations_by_path
.get(&self.normalized_relative_path)?
.clone(),
})
}
}
struct ExecutorAttributionCandidate<'a> {
matched: MatchedPluginScript<'a>,
contents: Vec<u8>,
}
#[derive(Clone, Copy)]
enum PluginVersionMatch {
Any,
Exact,
}
fn normalized_script_suffixes(script: &PathUri) -> Vec<String> {
let path = script.inferred_native_path_string().replace('\\', "/");
let components = path
.split('/')
.filter(|component| !component.is_empty())
.collect::<Vec<_>>();
(0..components.len())
.filter_map(|start| {
let suffix = components[start..].join("/");
is_safe_plugin_relative_path(&suffix).then_some(suffix)
})
.collect()
}
fn executor_plugin_root_matches(
script: &PathUri,
trusted: &TrustedPluginRoot,
relative_path: &str,
version_match: PluginVersionMatch,
) -> bool {
let relative_depth = relative_path.split('/').count();
let Some(root) = script.ancestors().nth(relative_depth) else {
return false;
};
let Some(cache_parent) = root.ancestors().nth(5) else {
return false;
};
let Ok(plugin_root) = cache_parent.join(&format!(
"plugins/cache/{}/{}",
trusted.plugin_id.marketplace_name, trusted.plugin_id.plugin_name
)) else {
return false;
};
match version_match {
PluginVersionMatch::Any => root.parent().as_ref() == Some(&plugin_root),
PluginVersionMatch::Exact => plugin_root
.join(&trusted.version)
.is_ok_and(|expected| expected == root),
}
}
/// Returns the structurally parsed arguments following a single script command.
/// Callers must keep the values in-process and must not log or serialize them.
pub fn command_script_arguments(command: &[String]) -> Option<Vec<String>> {
let command = single_plain_command(command)?;
Some(script_invocation(command.as_slice())?.arguments.to_vec())
}
/// Converts a path already proven to be below a trusted plugin root into the
/// only path shape that may leave the resolver: non-empty, relative, and
/// slash-separated with no traversal or platform-specific prefixes.
pub(crate) fn normalized_relative_script_path(relative_path: &Path) -> Option<String> {
let normalized = relative_path
.components()
.map(|component| {
let Component::Normal(component) = component else {
return None;
};
component.to_str()
})
.collect::<Option<Vec<_>>>()?
.join("/");
is_safe_plugin_relative_path(&normalized).then_some(normalized)
}
fn single_plain_command(command: &[String]) -> Option<Vec<String>> {
if let Some(commands) = parse_shell_lc_plain_commands(command) {
let [command] = commands.as_slice() else {
return None;
};
return single_plain_command(command);
}
if let Some(script) = windows_shell_script(command) {
let wrapper = ["sh".to_string(), "-lc".to_string(), script.to_string()];
return single_plain_command(&wrapper);
}
if extract_bash_command(command).is_some() {
return None;
}
Some(command.to_vec())
}
struct ScriptInvocation<'a> {
script: &'a str,
arguments: &'a [String],
}
fn script_invocation(command: &[String]) -> Option<ScriptInvocation<'_>> {
let [program, args @ ..] = command else {
return None;
};
if let Some(interpreter) = interpreter_name(program) {
return interpreter_script_invocation(&interpreter, args);
}
is_pathish(program).then_some(ScriptInvocation {
script: program,
arguments: args,
})
}
fn interpreter_name(program: &str) -> Option<String> {
let basename = executable_basename(program)?;
let basename = basename.to_ascii_lowercase();
let basename = basename.strip_suffix(".exe").unwrap_or(&basename);
matches!(
basename,
"bash"
| "node"
| "nodejs"
| "perl"
| "php"
| "powershell"
| "pwsh"
| "python"
| "python3"
| "ruby"
| "sh"
| "zsh"
)
.then(|| basename.to_string())
}
fn interpreter_script_invocation<'a>(
interpreter: &str,
args: &'a [String],
) -> Option<ScriptInvocation<'a>> {
if matches!(interpreter, "powershell" | "pwsh") {
let [file_flag, script, arguments @ ..] = args else {
return None;
};
return (file_flag.eq_ignore_ascii_case("-file") && !script.starts_with('-'))
.then_some(ScriptInvocation { script, arguments });
}
let mut args = args;
loop {
match args {
[separator, script, arguments @ ..]
if separator == "--" && !script.starts_with('-') =>
{
return Some(ScriptInvocation { script, arguments });
}
[flag, remaining @ ..] if safe_interpreter_flag(interpreter, flag) => {
args = remaining;
}
[script, arguments @ ..] if !script.starts_with('-') => {
return Some(ScriptInvocation { script, arguments });
}
_ => return None,
}
}
}
fn safe_interpreter_flag(interpreter: &str, flag: &str) -> bool {
matches!(
(interpreter, flag),
("python" | "python3", "-u") | ("bash" | "sh" | "zsh", "-e")
)
}
fn executable_basename(program: &str) -> Option<&str> {
program
.rsplit(['/', '\\'])
.next()
.filter(|basename| !basename.is_empty())
}
fn windows_shell_script(command: &[String]) -> Option<&str> {
let [program, args @ ..] = command else {
return None;
};
let basename = executable_basename(program)?.to_ascii_lowercase();
if matches!(basename.as_str(), "cmd" | "cmd.exe") {
let [flag, script] = args else {
return None;
};
return flag.eq_ignore_ascii_case("/c").then_some(script);
}
if !matches!(
basename.as_str(),
"powershell" | "powershell.exe" | "pwsh" | "pwsh.exe"
) {
return None;
}
let [flags @ .., command_flag, script] = args else {
return None;
};
if !matches!(
command_flag.to_ascii_lowercase().as_str(),
"-command" | "-c"
) {
return None;
}
flags
.iter()
.all(|flag| {
matches!(
flag.to_ascii_lowercase().as_str(),
"-nologo" | "-noprofile" | "-noninteractive"
)
})
.then_some(script)
}
#[cfg(test)]
#[path = "script_attribution_tests.rs"]
mod tests;