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, } /// 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, } /// 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 { 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 { 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::>(); 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 { 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::>(); 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 { 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 { 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 { 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 { self.local_candidate(command, cwd)?.metrics_operation() } fn local_candidate( &self, command: &[String], cwd: &AbsolutePathBuf, ) -> Option> { 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 { 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 { 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> { 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> { 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 { 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, } #[derive(Clone, Copy)] enum PluginVersionMatch { Any, Exact, } fn normalized_script_suffixes(script: &PathUri) -> Vec { let path = script.inferred_native_path_string().replace('\\', "/"); let components = path .split('/') .filter(|component| !component.is_empty()) .collect::>(); (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> { 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 { let normalized = relative_path .components() .map(|component| { let Component::Normal(component) = component else { return None; }; component.to_str() }) .collect::>>()? .join("/"); is_safe_plugin_relative_path(&normalized).then_some(normalized) } fn single_plain_command(command: &[String]) -> Option> { 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> { 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 { 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> { 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;