mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
Bind executor plugin measurements to the trusted plugin version (#46528)
## Why Identical script contents across plugin versions do not establish that the same measurement declaration applies. Resolving measurements through generic command attribution could use a different version's declaration, while shared helper paths could make attribution ambiguous. ## What changed - Match canonical executor paths against trusted plugin identities before comparing script contents. Require an exact version match for measurements while retaining attribution across versions with matching contents. - Keep measurement declarations bound to the selected trusted root, and allow distinct versions to coexist when extending trusted roots. - Add `PluginMeasurementTarget` to extract an untrusted plugin/version hint from canonical remote cache paths, respecting Windows and POSIX path conventions. - Skip executor lookups for unrelated scripts so attribution does not wait for executor provisioning. - Increase the login unit test timeout in Bazel to `long`. ## Testing Add regression coverage for multiple plugins and versions, canonical aliases, symlink escapes, path casing, and executor lookup avoidance. Add a remote execution integration test verifying that mismatched versions retain command attribution but receive no metrics sidecar, and matching versions use the trusted measurement declaration. GitOrigin-RevId: d63430b37bf2de360c8d09af7f7453a5b5024d72
This commit is contained in:
@@ -108,5 +108,6 @@ pub use remote::RecommendedPlugin;
|
||||
pub use remote::RecommendedPluginsMode;
|
||||
pub use remote_metadata::remote_catalog_metadata_eq;
|
||||
pub use script_attribution::PluginCommandAttribution;
|
||||
pub use script_attribution::PluginMeasurementTarget;
|
||||
pub use script_attribution::TrustedPluginRoots;
|
||||
pub use script_attribution::command_script_arguments;
|
||||
|
||||
@@ -16,6 +16,7 @@ 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;
|
||||
@@ -25,6 +26,7 @@ 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;
|
||||
@@ -34,6 +36,7 @@ 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>,
|
||||
}
|
||||
@@ -63,7 +66,81 @@ 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 {
|
||||
/// 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 {
|
||||
@@ -85,9 +162,11 @@ impl TrustedPluginRoots {
|
||||
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,
|
||||
@@ -175,6 +254,24 @@ impl TrustedPluginRoots {
|
||||
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() {
|
||||
@@ -194,8 +291,8 @@ impl TrustedPluginRoots {
|
||||
.strip_prefix(root.root.as_path())
|
||||
.ok()
|
||||
.filter(|relative_path| !relative_path.as_os_str().is_empty())?;
|
||||
Some(PluginCommandAttribution {
|
||||
plugin_id: root.plugin_id.clone(),
|
||||
Some(MatchedPluginScript {
|
||||
root,
|
||||
normalized_relative_path: normalized_relative_script_path(relative_path)?,
|
||||
})
|
||||
});
|
||||
@@ -203,58 +300,67 @@ impl TrustedPluginRoots {
|
||||
matches.next().is_none().then_some(attribution)
|
||||
}
|
||||
|
||||
/// Resolves one exact command to one trusted manifest-declared operation.
|
||||
pub fn resolve_metrics_operation(
|
||||
&self,
|
||||
command: &[String],
|
||||
cwd: &AbsolutePathBuf,
|
||||
) -> Option<ResolvedPluginMetricsOperation> {
|
||||
let attribution = self.resolve_attribution(command, cwd)?;
|
||||
self.metrics_operation_for_attribution(attribution)
|
||||
}
|
||||
|
||||
fn metrics_operation_for_attribution(
|
||||
&self,
|
||||
attribution: PluginCommandAttribution,
|
||||
) -> Option<ResolvedPluginMetricsOperation> {
|
||||
let mut matches = self.roots.iter().filter_map(|root| {
|
||||
(root.plugin_id == attribution.plugin_id)
|
||||
.then(|| {
|
||||
root.metrics_operations_by_path
|
||||
.get(&attribution.normalized_relative_path)
|
||||
})
|
||||
.flatten()
|
||||
});
|
||||
let operation = matches.next()?.clone();
|
||||
matches
|
||||
.next()
|
||||
.is_none()
|
||||
.then_some(ResolvedPluginMetricsOperation {
|
||||
plugin_id: attribution.plugin_id,
|
||||
operation,
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolves a trusted script on the selected executor filesystem.
|
||||
///
|
||||
/// Remote commands can use a path convention that the app-server host cannot
|
||||
/// canonicalize. Match the target-native path to one trusted local plugin
|
||||
/// script, then require the executor-side file to have the same contents.
|
||||
/// 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()?;
|
||||
let candidate = self.local_candidate_for_executor_script(&script)?;
|
||||
// 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()?;
|
||||
if !executor_plugin_root_matches(&script, &candidate.attribution) {
|
||||
let candidates = self.local_candidates_for_executor_script(&script, version_match);
|
||||
if candidates.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let metadata = file_system
|
||||
@@ -265,62 +371,98 @@ impl TrustedPluginRoots {
|
||||
)
|
||||
.await
|
||||
.ok()?;
|
||||
if !metadata.is_file || metadata.size != candidate.contents.len() as u64 {
|
||||
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()?;
|
||||
(contents == candidate.contents).then_some(candidate.attribution)
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves one trusted executor script to one manifest-declared operation.
|
||||
pub async fn resolve_metrics_operation_in_filesystem(
|
||||
&self,
|
||||
command: &[String],
|
||||
cwd: &PathUri,
|
||||
file_system: &dyn ExecutorFileSystem,
|
||||
) -> Option<ResolvedPluginMetricsOperation> {
|
||||
let attribution = self
|
||||
.resolve_executor_attribution(command, cwd, file_system)
|
||||
.await?;
|
||||
self.metrics_operation_for_attribution(attribution)
|
||||
}
|
||||
|
||||
fn local_candidate_for_executor_script(
|
||||
fn local_candidates_for_executor_script(
|
||||
&self,
|
||||
script: &PathUri,
|
||||
) -> Option<ExecutorAttributionCandidate> {
|
||||
version_match: PluginVersionMatch,
|
||||
) -> Vec<ExecutorAttributionCandidate<'_>> {
|
||||
let suffixes = normalized_script_suffixes(script);
|
||||
let mut matches = self.roots.iter().filter_map(|root| {
|
||||
let (script, normalized_relative_path) = suffixes.iter().find_map(|suffix| {
|
||||
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 {
|
||||
attribution: PluginCommandAttribution {
|
||||
plugin_id: root.plugin_id.clone(),
|
||||
normalized_relative_path,
|
||||
},
|
||||
contents: std::fs::read(script.as_path()).ok()?,
|
||||
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()?,
|
||||
})
|
||||
})
|
||||
});
|
||||
let candidate = matches.next()?;
|
||||
matches.next().is_none().then_some(candidate)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
struct ExecutorAttributionCandidate {
|
||||
attribution: PluginCommandAttribution,
|
||||
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
|
||||
@@ -335,24 +477,31 @@ fn normalized_script_suffixes(script: &PathUri) -> Vec<String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn executor_plugin_root_matches(script: &PathUri, attribution: &PluginCommandAttribution) -> bool {
|
||||
let relative_depth = attribution.normalized_relative_path.split('/').count();
|
||||
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 path = root.inferred_native_path_string().replace('\\', "/");
|
||||
let components = path
|
||||
.split('/')
|
||||
.filter(|component| !component.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
let [.., plugins, cache, marketplace, plugin, version] = components.as_slice() else {
|
||||
let Some(cache_parent) = root.ancestors().nth(5) else {
|
||||
return false;
|
||||
};
|
||||
*plugins == "plugins"
|
||||
&& *cache == "cache"
|
||||
&& *marketplace == attribution.plugin_id.marketplace_name.as_str()
|
||||
&& *plugin == attribution.plugin_id.plugin_name.as_str()
|
||||
&& !version.is_empty()
|
||||
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.
|
||||
|
||||
@@ -10,15 +10,24 @@ use crate::test_support::TEST_CURATED_PLUGIN_SHA;
|
||||
use crate::test_support::write_curated_plugin_sha_with;
|
||||
use crate::test_support::write_openai_api_curated_marketplace;
|
||||
use crate::test_support::write_openai_curated_marketplace;
|
||||
use codex_exec_server::ExecServerError;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_exec_server::NoiseChannelPublicKey;
|
||||
use codex_exec_server::NoiseRendezvousConnectBundle;
|
||||
use codex_exec_server::NoiseRendezvousConnectProvider;
|
||||
use codex_exec_server_test_support::environment_manager_without_environments;
|
||||
use codex_plugin::PluginLoadOutcome;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use codex_utils_plugins::SkillDiscoveryMode;
|
||||
use futures::FutureExt;
|
||||
use futures::future::BoxFuture;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
const ENABLED: bool = true;
|
||||
const DISABLED: bool = false;
|
||||
@@ -151,6 +160,7 @@ fn resolves_primary_runtime_scripts_from_the_installed_plugin_cache() {
|
||||
let roots = TrustedPluginRoots {
|
||||
roots: vec![TrustedPluginRoot {
|
||||
plugin_id: plugin_id.clone(),
|
||||
version: "0.1.29".to_string(),
|
||||
metrics_operations_by_path: BTreeMap::new(),
|
||||
root: plugin_root.canonicalize().expect("canonical plugin root"),
|
||||
}],
|
||||
@@ -193,22 +203,20 @@ async fn resolves_relocated_script_through_executor_filesystem() {
|
||||
let executor = TempDir::new().expect("executor temp dir");
|
||||
let executor_root = executor
|
||||
.path()
|
||||
.join("plugins/cache/openai-curated/sample/remote-version");
|
||||
.join("plugins/cache/openai-curated/sample")
|
||||
.join(curated_plugin_cache_version(TEST_CURATED_PLUGIN_SHA));
|
||||
let executor_script = executor_root.join("scripts/run.py");
|
||||
fs::create_dir_all(executor_script.parent().expect("script parent"))
|
||||
.expect("create executor scripts");
|
||||
fs::copy(script.as_path(), &executor_script).expect("copy script to executor");
|
||||
let cwd = PathUri::from_host_native_path(&executor_root).expect("executor root URI");
|
||||
let environment =
|
||||
codex_exec_server::Environment::create_for_tests(/*exec_server_url*/ None)
|
||||
.expect("local executor environment");
|
||||
|
||||
assert_eq!(
|
||||
roots
|
||||
.resolve_executor_attribution(
|
||||
&command(&["python", "scripts/run.py"]),
|
||||
&cwd,
|
||||
environment.get_filesystem().as_ref(),
|
||||
LOCAL_FS.as_ref(),
|
||||
)
|
||||
.await,
|
||||
Some(PluginCommandAttribution {
|
||||
@@ -223,7 +231,7 @@ async fn resolves_relocated_script_through_executor_filesystem() {
|
||||
.resolve_executor_attribution(
|
||||
&command(&["python", "scripts/run.py"]),
|
||||
&cwd,
|
||||
environment.get_filesystem().as_ref(),
|
||||
LOCAL_FS.as_ref(),
|
||||
)
|
||||
.await,
|
||||
None
|
||||
@@ -232,17 +240,35 @@ async fn resolves_relocated_script_through_executor_filesystem() {
|
||||
|
||||
#[test]
|
||||
fn recognizes_windows_executor_plugin_cache_root() {
|
||||
let attribution = PluginCommandAttribution {
|
||||
let reference = TempDir::new().expect("reference root");
|
||||
let trusted = TrustedPluginRoot {
|
||||
plugin_id: PluginId::parse("presentations@openai-primary-runtime").expect("plugin id"),
|
||||
normalized_relative_path:
|
||||
"skills/presentations/container_tools/mark_artifact_operation_started.mjs".to_string(),
|
||||
version: "0.1.29".to_string(),
|
||||
root: path(reference.path()),
|
||||
metrics_operations_by_path: BTreeMap::new(),
|
||||
};
|
||||
let relative_path = "skills/presentations/container_tools/mark_artifact_operation_started.mjs";
|
||||
let script = PathUri::parse(
|
||||
"file:///C:/Users/user/.codex/plugins/cache/openai-primary-runtime/presentations/0.1.29/skills/presentations/container_tools/mark_artifact_operation_started.mjs",
|
||||
)
|
||||
.expect("Windows script URI");
|
||||
|
||||
assert!(executor_plugin_root_matches(&script, &attribution));
|
||||
assert!(executor_plugin_root_matches(
|
||||
&script,
|
||||
&trusted,
|
||||
relative_path,
|
||||
PluginVersionMatch::Exact,
|
||||
));
|
||||
let wrong_version = PathUri::parse(
|
||||
"file:///C:/Users/user/.codex/plugins/cache/openai-primary-runtime/presentations/0.1.28/skills/presentations/container_tools/mark_artifact_operation_started.mjs",
|
||||
)
|
||||
.expect("other-version Windows script URI");
|
||||
assert!(!executor_plugin_root_matches(
|
||||
&wrong_version,
|
||||
&trusted,
|
||||
relative_path,
|
||||
PluginVersionMatch::Exact,
|
||||
));
|
||||
}
|
||||
fn assert_invalid_metrics_manifest(codex_home: &Path, root: &AbsolutePathBuf, manifest: &str) {
|
||||
fs::write(root.join("analytics.yaml"), manifest).expect("write analytics manifest");
|
||||
@@ -321,17 +347,20 @@ fn trusted_roots_require_verified_curated_or_remote_cache() {
|
||||
vec![
|
||||
TrustedPluginRoot {
|
||||
plugin_id: PluginId::parse("sample@openai-curated").expect("plugin id"),
|
||||
version: curated_plugin_cache_version(TEST_CURATED_PLUGIN_SHA),
|
||||
metrics_operations_by_path: BTreeMap::new(),
|
||||
root: root.canonicalize().expect("canonical root"),
|
||||
},
|
||||
TrustedPluginRoot {
|
||||
plugin_id: PluginId::parse("api-sample@openai-api-curated").expect("plugin id"),
|
||||
version: curated_plugin_cache_version(TEST_CURATED_PLUGIN_SHA),
|
||||
metrics_operations_by_path: BTreeMap::new(),
|
||||
root: api_root.canonicalize().expect("canonical root"),
|
||||
},
|
||||
TrustedPluginRoot {
|
||||
plugin_id: PluginId::parse("remote-sample@openai-curated-remote")
|
||||
.expect("plugin id"),
|
||||
version: "1.2.3".to_string(),
|
||||
metrics_operations_by_path: BTreeMap::new(),
|
||||
root: remote_root.canonicalize().expect("canonical root"),
|
||||
},
|
||||
@@ -651,11 +680,13 @@ fn rejects_ambiguous_commands_overlaps_and_symlink_escapes() {
|
||||
roots: vec![
|
||||
TrustedPluginRoot {
|
||||
plugin_id: PluginId::parse("sample@openai-curated").expect("plugin id"),
|
||||
version: curated_plugin_cache_version(TEST_CURATED_PLUGIN_SHA),
|
||||
metrics_operations_by_path: BTreeMap::new(),
|
||||
root: root.canonicalize().expect("canonical root"),
|
||||
},
|
||||
TrustedPluginRoot {
|
||||
plugin_id: PluginId::parse("nested@openai-curated").expect("plugin id"),
|
||||
version: curated_plugin_cache_version(TEST_CURATED_PLUGIN_SHA),
|
||||
metrics_operations_by_path: BTreeMap::new(),
|
||||
root: root.join("scripts").canonicalize().expect("nested root"),
|
||||
},
|
||||
@@ -688,3 +719,461 @@ fn rejects_ambiguous_commands_overlaps_and_symlink_escapes() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const REFERENCE_HELPER: &str = "scripts/install-dependencies.sh";
|
||||
|
||||
fn reference_metrics_operation(variant: &str) -> PluginMetricsOperation {
|
||||
PluginMetricsOperation {
|
||||
operation_name: "dependency_install".to_string(),
|
||||
measurements: BTreeMap::from([(
|
||||
"duration_ms".to_string(),
|
||||
PluginMeasurementDefinition {
|
||||
enum_dimensions: BTreeMap::from([(
|
||||
"variant".to_string(),
|
||||
BTreeSet::from([variant.to_string()]),
|
||||
)]),
|
||||
},
|
||||
)]),
|
||||
}
|
||||
}
|
||||
|
||||
fn reference_identity_fixture(root: &Path, variant: &str, contents: &str) -> AbsolutePathBuf {
|
||||
fs::create_dir_all(root.join("scripts")).expect("create reference scripts");
|
||||
fs::write(root.join(REFERENCE_HELPER), contents).expect("write helper");
|
||||
fs::write(
|
||||
root.join("analytics.yaml"),
|
||||
format!(
|
||||
"version: 1\noperations:\n dependency_install:\n path: ./{REFERENCE_HELPER}\n measurements:\n duration_ms:\n dimensions:\n variant: [{variant}]\n"
|
||||
),
|
||||
)
|
||||
.expect("write operation manifest");
|
||||
path(root).canonicalize().expect("canonical reference root")
|
||||
}
|
||||
|
||||
fn reference_fixture(bundles: &[(&str, &str, &str, &str)]) -> (TempDir, TrustedPluginRoots) {
|
||||
let home = TempDir::new().expect("references");
|
||||
let roots = TrustedPluginRoots {
|
||||
roots: bundles
|
||||
.iter()
|
||||
.map(|(name, version, variant, contents)| {
|
||||
let root = reference_identity_fixture(
|
||||
&home.path().join(name).join(version),
|
||||
variant,
|
||||
contents,
|
||||
);
|
||||
TrustedPluginRoot {
|
||||
plugin_id: PluginId::parse(&format!("{name}@openai-curated-remote"))
|
||||
.expect("plugin id"),
|
||||
version: version.to_string(),
|
||||
metrics_operations_by_path: load_plugin_metrics_operations(&root)
|
||||
.expect("valid fixture analytics"),
|
||||
root,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
(home, roots)
|
||||
}
|
||||
|
||||
fn executor_plugin(home: &Path, name: &str, version: &str, contents: &str) -> AbsolutePathBuf {
|
||||
reference_identity_fixture(
|
||||
&home.join(format!(
|
||||
"plugins/cache/openai-curated-remote/{name}/{version}"
|
||||
)),
|
||||
"untrusted_executor_metadata",
|
||||
contents,
|
||||
)
|
||||
}
|
||||
|
||||
// Check both APIs: ordinary attribution can survive version differences, while
|
||||
// measurements must use the exact authenticated declaration (never executor YAML).
|
||||
async fn assert_executor_resolution(
|
||||
roots: &TrustedPluginRoots,
|
||||
command: &[String],
|
||||
cwd: &PathUri,
|
||||
plugin_id: &PluginId,
|
||||
variant: Option<&str>,
|
||||
) {
|
||||
assert_eq!(
|
||||
roots
|
||||
.resolve_executor_attribution(command, cwd, LOCAL_FS.as_ref())
|
||||
.await,
|
||||
Some(PluginCommandAttribution {
|
||||
plugin_id: plugin_id.clone(),
|
||||
normalized_relative_path: REFERENCE_HELPER.to_string(),
|
||||
}),
|
||||
"attribution for {command:?} at {cwd:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
roots
|
||||
.resolve_metrics_operation_in_filesystem(command, cwd, LOCAL_FS.as_ref())
|
||||
.await,
|
||||
variant.map(|variant| ResolvedPluginMetricsOperation {
|
||||
plugin_id: plugin_id.clone(),
|
||||
operation: reference_metrics_operation(variant),
|
||||
}),
|
||||
"measurements for {command:?} at {cwd:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn executor_cache_identity_obeys_windows_and_posix_case_conventions() {
|
||||
let (_reference, roots) =
|
||||
reference_fixture(&[("sites", "2.0.0-RC1", "selected", "printf shared\\n\n")]);
|
||||
let plugin_id = PluginId::parse("sites@openai-curated-remote").expect("plugin id");
|
||||
for prefix in ["file:///C:/Users/user/.codex", "file://server/share/.codex"] {
|
||||
let script = PathUri::parse(&format!(
|
||||
"{prefix}/PLUGINS/CACHE/OPENAI-CURATED-REMOTE/SITES/2.0.0-rc1/{REFERENCE_HELPER}"
|
||||
))
|
||||
.expect("Windows script");
|
||||
let target = PluginMeasurementTarget::from_script_path(&script)
|
||||
.expect("Windows cache preselection follows the resolver's path identity");
|
||||
assert_eq!(
|
||||
target,
|
||||
PluginMeasurementTarget {
|
||||
plugin_id: plugin_id.clone(),
|
||||
version: "2.0.0-rc1".to_string(),
|
||||
path_convention: PathConvention::Windows,
|
||||
}
|
||||
);
|
||||
let equivalent = PathUri::parse(&format!(
|
||||
"{prefix}/plugins/cache/openai-curated-remote/sites/2.0.0-RC1/{REFERENCE_HELPER}"
|
||||
))
|
||||
.expect("equivalent Windows script");
|
||||
assert_eq!(
|
||||
PluginMeasurementTarget::from_script_path(&equivalent),
|
||||
Some(target)
|
||||
);
|
||||
let candidates =
|
||||
roots.local_candidates_for_executor_script(&script, PluginVersionMatch::Exact);
|
||||
let candidate = candidates
|
||||
.first()
|
||||
.expect("Windows identity casing must not prevent canonicalization");
|
||||
assert_eq!(
|
||||
candidate.matched.metrics_operation(),
|
||||
Some(ResolvedPluginMetricsOperation {
|
||||
plugin_id: plugin_id.clone(),
|
||||
operation: reference_metrics_operation("selected"),
|
||||
})
|
||||
);
|
||||
}
|
||||
for script in [
|
||||
format!(
|
||||
"file:///home/user/.codex/PLUGINS/CACHE/OPENAI-CURATED-REMOTE/SITES/2.0.0-rc1/{REFERENCE_HELPER}"
|
||||
),
|
||||
format!(
|
||||
"file:///home/user/.codex/plugins/cache/openai-curated-remote/SITES/2.0.0-RC1/{REFERENCE_HELPER}"
|
||||
),
|
||||
format!(
|
||||
"file:///C:/Users/user/.codex/PLUGINS/CACHE/OPENAI-CURATED-REMOTE/SITES/2.0.0-rc2/{REFERENCE_HELPER}"
|
||||
),
|
||||
format!(
|
||||
"file:///C:/Users/user/.codex/PLUGINS/CACHE/OPENAI-CURATED-REMOTE/OTHER/2.0.0-rc1/{REFERENCE_HELPER}"
|
||||
),
|
||||
] {
|
||||
let script = PathUri::parse(&script).expect("script");
|
||||
let path_convention = script.infer_path_convention().expect("path convention");
|
||||
let expected = PluginMeasurementTarget {
|
||||
plugin_id: plugin_id.clone(),
|
||||
version: match path_convention {
|
||||
PathConvention::Windows => "2.0.0-rc1",
|
||||
PathConvention::Posix => "2.0.0-RC1",
|
||||
}
|
||||
.to_string(),
|
||||
path_convention,
|
||||
};
|
||||
assert_ne!(
|
||||
PluginMeasurementTarget::from_script_path(&script),
|
||||
Some(expected),
|
||||
"preselection must not match a differently cased POSIX identity or another version"
|
||||
);
|
||||
assert!(
|
||||
roots
|
||||
.local_candidates_for_executor_script(&script, PluginVersionMatch::Exact)
|
||||
.is_empty(),
|
||||
"POSIX casing and different Windows versions must remain distinct"
|
||||
);
|
||||
}
|
||||
for version in ["2.0.0-RC1", "2.0.0-rc1"] {
|
||||
let script = PathUri::parse(&format!(
|
||||
"file:///home/user/.codex/plugins/cache/openai-curated-remote/sites/{version}/{REFERENCE_HELPER}"
|
||||
))
|
||||
.expect("POSIX script");
|
||||
let target =
|
||||
PluginMeasurementTarget::from_script_path(&script).expect("POSIX cache preselection");
|
||||
assert_eq!(
|
||||
target,
|
||||
PluginMeasurementTarget {
|
||||
plugin_id: plugin_id.clone(),
|
||||
version: version.to_string(),
|
||||
path_convention: PathConvention::Posix,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extending_trusted_roots_preserves_existing_plugins_and_deduplicates_snapshots() {
|
||||
let contents = "printf shared\\n\n";
|
||||
let (_frontend, mut roots) = reference_fixture(&[("sites", "1.2.3", "primary", contents)]);
|
||||
let original = roots.clone();
|
||||
let (_snapshot, additional) = reference_fixture(&[
|
||||
("sites", "1.2.3", "copied", contents),
|
||||
("other", "1.2.3", "additional", contents),
|
||||
]);
|
||||
|
||||
roots.extend(&additional);
|
||||
let expected = TrustedPluginRoots {
|
||||
roots: vec![original.roots[0].clone(), additional.roots[1].clone()],
|
||||
};
|
||||
assert_eq!(roots, expected);
|
||||
roots.extend(&additional);
|
||||
assert_eq!(roots, expected, "repeated snapshots must remain idempotent");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authenticated_v2_reference_coexists_with_loaded_frontend_v1() {
|
||||
let frontend = TempDir::new().expect("frontend");
|
||||
let v1 = installed_remote_plugin_root(frontend.path(), "sites");
|
||||
reference_identity_fixture(v1.as_path(), "install_v1", "printf v1\\n\n");
|
||||
let plugin_id = PluginId::parse("sites@openai-curated-remote").expect("plugin id");
|
||||
let mut roots = roots_for(
|
||||
frontend.path(),
|
||||
vec![loaded_plugin(&plugin_id.as_key(), v1.as_path(), ENABLED)],
|
||||
);
|
||||
let (_reference, references) =
|
||||
reference_fixture(&[("sites", "2.0.0", "install_v2", "printf v2\\n\n")]);
|
||||
roots.extend(&references);
|
||||
// The operation must stay bound to the selected root when two versions share
|
||||
// a plugin ID and helper path, for both local and executor resolution.
|
||||
for (root, variant) in [
|
||||
(&v1, "install_v1"),
|
||||
(&references.roots[0].root, "install_v2"),
|
||||
] {
|
||||
assert_eq!(
|
||||
roots.resolve_metrics_operation(&command(&["sh", REFERENCE_HELPER]), root),
|
||||
Some(ResolvedPluginMetricsOperation {
|
||||
plugin_id: plugin_id.clone(),
|
||||
operation: reference_metrics_operation(variant),
|
||||
})
|
||||
);
|
||||
}
|
||||
let executor = TempDir::new().expect("executor");
|
||||
let root = executor_plugin(executor.path(), "sites", "2.0.0", "printf v2\\n\n");
|
||||
let cwd = PathUri::from_host_native_path(root.as_path()).expect("executor URI");
|
||||
assert_executor_resolution(
|
||||
&roots,
|
||||
&command(&["sh", REFERENCE_HELPER]),
|
||||
&cwd,
|
||||
&plugin_id,
|
||||
Some("install_v2"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn same_helper_path_in_two_plugins_resolves_by_executor_identity() {
|
||||
// Identical bytes: neither suffix nor content identifies a plugin.
|
||||
let contents = "printf shared\\n\n";
|
||||
let (_reference, roots) = reference_fixture(&[
|
||||
("sites", "2.0.0", "sites_install", contents),
|
||||
("other", "2.0.0", "other_install", contents),
|
||||
]);
|
||||
let executor = TempDir::new().expect("executor");
|
||||
for (name, variant) in [("sites", "sites_install"), ("other", "other_install")] {
|
||||
let root = executor_plugin(executor.path(), name, "2.0.0", contents);
|
||||
let cwd = PathUri::from_host_native_path(root.as_path()).expect("executor URI");
|
||||
let plugin_id =
|
||||
PluginId::parse(&format!("{name}@openai-curated-remote")).expect("plugin id");
|
||||
assert_executor_resolution(
|
||||
&roots,
|
||||
&command(&["sh", REFERENCE_HELPER]),
|
||||
&cwd,
|
||||
&plugin_id,
|
||||
Some(variant),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn identical_script_bytes_do_not_authorize_another_executor_version() {
|
||||
let contents = "printf shared\\n\n";
|
||||
let (_reference, roots) =
|
||||
reference_fixture(&[("sites", "2.0.0", "v2_only_operation", contents)]);
|
||||
let plugin_id = PluginId::parse("sites@openai-curated-remote").expect("plugin id");
|
||||
let executor = TempDir::new().expect("executor");
|
||||
for version in ["1.2.3", "2.0.0"] {
|
||||
let root = executor_plugin(executor.path(), "sites", version, contents);
|
||||
let cwd = PathUri::from_host_native_path(root.as_path()).expect("executor URI");
|
||||
let candidate = command(&["bash", "-lc", &format!("sh {REFERENCE_HELPER}")]);
|
||||
assert_eq!(
|
||||
PluginMeasurementTarget::from_command(&candidate, &cwd, LOCAL_FS.as_ref()).await,
|
||||
Some(PluginMeasurementTarget {
|
||||
plugin_id: plugin_id.clone(),
|
||||
version: version.to_string(),
|
||||
path_convention: PathConvention::native(),
|
||||
})
|
||||
);
|
||||
assert_executor_resolution(
|
||||
&roots,
|
||||
&command(&["sh", REFERENCE_HELPER]),
|
||||
&cwd,
|
||||
&plugin_id,
|
||||
(version == "2.0.0").then_some("v2_only_operation"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn executor_symlink_cannot_change_authenticated_plugin_or_version() {
|
||||
let contents = "printf shared\\n\n";
|
||||
let (_reference, roots) =
|
||||
reference_fixture(&[("sites", "2.0.0", "v2_only_operation", contents)]);
|
||||
let executor = TempDir::new().expect("executor");
|
||||
let original = executor
|
||||
.path()
|
||||
.join("plugins/cache/openai-curated-remote/sites/2.0.0");
|
||||
fs::create_dir_all(original.join("scripts")).expect("original scripts directory");
|
||||
let cwd = PathUri::from_host_native_path(&original).expect("original executor URI");
|
||||
for target_identity in [
|
||||
"openai-curated-remote/other/2.0.0",
|
||||
"openai-curated-remote/sites/1.2.3",
|
||||
"openai-curated/sites/2.0.0",
|
||||
"../outside-cache",
|
||||
] {
|
||||
let target = reference_identity_fixture(
|
||||
&executor.path().join("plugins/cache").join(target_identity),
|
||||
"ignored_executor_metadata",
|
||||
contents,
|
||||
);
|
||||
std::os::unix::fs::symlink(
|
||||
target.join(REFERENCE_HELPER),
|
||||
original.join(REFERENCE_HELPER),
|
||||
)
|
||||
.expect("redirect helper to another identity");
|
||||
assert_eq!(
|
||||
roots
|
||||
.resolve_metrics_operation_in_filesystem(
|
||||
&command(&["sh", REFERENCE_HELPER]),
|
||||
&cwd,
|
||||
LOCAL_FS.as_ref(),
|
||||
)
|
||||
.await,
|
||||
None,
|
||||
"canonical identity changed to {target_identity}"
|
||||
);
|
||||
fs::remove_file(original.join(REFERENCE_HELPER)).expect("remove helper symlink");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn executor_aliases_use_canonical_plugin_paths() {
|
||||
let contents = "printf shared\\n\n";
|
||||
let (_reference, roots) = reference_fixture(&[
|
||||
("sites", "1.2.3", "older", contents),
|
||||
("sites", "2.0.0", "selected", contents),
|
||||
]);
|
||||
let older_roots = TrustedPluginRoots {
|
||||
roots: vec![roots.roots[0].clone()],
|
||||
};
|
||||
let plugin_id = PluginId::parse("sites@openai-curated-remote").expect("plugin id");
|
||||
let executor = TempDir::new().expect("executor");
|
||||
let target = executor_plugin(executor.path(), "sites", "2.0.0", contents);
|
||||
let alias = executor.path().join("plugin-link");
|
||||
std::os::unix::fs::symlink(target.as_path(), &alias).expect("directory alias");
|
||||
let version_alias = executor
|
||||
.path()
|
||||
.join("plugins/cache/openai-curated-remote/sites/1.2.3/scripts/renamed-entry.sh");
|
||||
fs::create_dir_all(version_alias.parent().expect("alias parent")).expect("V1 alias directory");
|
||||
std::os::unix::fs::symlink(target.join(REFERENCE_HELPER), &version_alias).expect("V1 alias");
|
||||
let cwd = PathUri::from_host_native_path(executor.path()).expect("executor URI");
|
||||
// An already loaded reference still supports directory aliases. Preparation
|
||||
// only probes cache paths, so ordinary workspace scripts need no executor I/O.
|
||||
assert_executor_resolution(
|
||||
&roots,
|
||||
&command(&["sh", &alias.join(REFERENCE_HELPER).to_string_lossy()]),
|
||||
&cwd,
|
||||
&plugin_id,
|
||||
Some("selected"),
|
||||
)
|
||||
.await;
|
||||
let command = command(&["sh", &version_alias.to_string_lossy()]);
|
||||
assert_eq!(
|
||||
PluginMeasurementTarget::from_command(&command, &cwd, LOCAL_FS.as_ref()).await,
|
||||
Some(PluginMeasurementTarget {
|
||||
plugin_id: plugin_id.clone(),
|
||||
version: "2.0.0".to_string(),
|
||||
path_convention: PathConvention::Posix,
|
||||
})
|
||||
);
|
||||
assert_executor_resolution(&roots, &command, &cwd, &plugin_id, Some("selected")).await;
|
||||
assert_eq!(
|
||||
older_roots
|
||||
.resolve_metrics_operation_in_filesystem(&command, &cwd, LOCAL_FS.as_ref())
|
||||
.await,
|
||||
None,
|
||||
"a V1-shaped alias and identical bytes cannot authorize V2 metrics"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn measurement_target_rejects_inline_commands_and_local_versions() -> anyhow::Result<()> {
|
||||
let executor = TempDir::new()?;
|
||||
let root = reference_identity_fixture(
|
||||
&executor
|
||||
.path()
|
||||
.join("plugins/cache/openai-curated-remote/sites/local"),
|
||||
"ignored",
|
||||
"printf fixture\n",
|
||||
);
|
||||
let cwd = PathUri::from_host_native_path(root.as_path())?;
|
||||
for args in [&["node", "-e", "42"][..], &["sh", REFERENCE_HELPER][..]] {
|
||||
let command = command(args);
|
||||
let fs = codex_exec_server::LOCAL_FS.as_ref();
|
||||
assert_eq!(
|
||||
PluginMeasurementTarget::from_command(&command, &cwd, fs).await,
|
||||
None
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct UnusedConnectProvider;
|
||||
|
||||
impl NoiseRendezvousConnectProvider for UnusedConnectProvider {
|
||||
fn connect_bundle(
|
||||
&self,
|
||||
_: NoiseChannelPublicKey,
|
||||
) -> BoxFuture<'_, Result<NoiseRendezvousConnectBundle, ExecServerError>> {
|
||||
panic!("unrelated script must not connect to the executor")
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unrelated_scripts_skip_executor_lookup() -> anyhow::Result<()> {
|
||||
let (_reference, roots) =
|
||||
reference_fixture(&[("sites", "2.0.0", "selected", "printf shared\\n\n")]);
|
||||
let manager = environment_manager_without_environments();
|
||||
let pending = manager.materialize_pending_noise_environment(
|
||||
"tools".to_string(),
|
||||
Arc::new(UnusedConnectProvider),
|
||||
)?;
|
||||
let fs = pending.get_filesystem();
|
||||
let cwd = PathUri::parse("file:///workspace")?;
|
||||
let command = command(&["node", "build.js"]);
|
||||
// An executor lookup waits for provisioning; unrelated scripts must finish immediately.
|
||||
assert_eq!(
|
||||
futures::future::join3(
|
||||
roots.resolve_executor_attribution(&command, &cwd, fs.as_ref()),
|
||||
roots.resolve_metrics_operation_in_filesystem(&command, &cwd, fs.as_ref()),
|
||||
PluginMeasurementTarget::from_command(&command, &cwd, fs.as_ref()),
|
||||
)
|
||||
.now_or_never(),
|
||||
Some((None, None, None)),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -498,6 +498,167 @@ async fn persisted_remote_plugin_command_attribution_flows_through_turn_context(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn remote_plugin_measurements_require_the_frontend_version() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
// This fixture needs a real remote transport with separately controlled frontend
|
||||
// and executor caches, rather than the runner's automatic executor selection.
|
||||
let executor = super::multi_exec_server_sandbox::ExecServerProcess::start().await?;
|
||||
let server = start_mock_server().await;
|
||||
let home = Arc::new(TempDir::new()?);
|
||||
let frontend_script = write_remote_plugin_script_and_config(home.as_ref());
|
||||
let script = r#"if [ -n "${CODEX_PLUGIN_METRICS_OUTPUT:-}" ]; then
|
||||
printf '%s' '{"version":1,"measurements":[{"name":"duration_ms","value":7,"dimensions":{"release":"v1"}}]}' > "$CODEX_PLUGIN_METRICS_OUTPUT"
|
||||
else
|
||||
printf 'no metrics sidecar\n'
|
||||
fi
|
||||
"#;
|
||||
std::fs::write(&frontend_script, script)?;
|
||||
std::fs::write(
|
||||
frontend_script
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("analytics.yaml"),
|
||||
"version: 1\noperations:\n dependency_install:\n path: ./scripts/run.sh\n measurements:\n duration_ms:\n dimensions:\n release: [v1]\n",
|
||||
)?;
|
||||
let executor_home = TempDir::new()?;
|
||||
let mut responses = Vec::new();
|
||||
for (version, call_id) in [("2.0.0", "wrong-version"), ("1.2.3", "matching-version")] {
|
||||
let root = executor_home.path().join(format!(
|
||||
"plugins/cache/openai-curated-remote/sample/{version}"
|
||||
));
|
||||
std::fs::create_dir_all(root.join("scripts"))?;
|
||||
let path = root.join("scripts/run.sh");
|
||||
std::fs::write(&path, script)?;
|
||||
// The executor's declaration is never an authority, even for matching bytes.
|
||||
std::fs::write(
|
||||
root.join("analytics.yaml"),
|
||||
"version: 1\noperations: {untrusted: {path: ./scripts/run.sh, measurements: {other: {}}}}\n",
|
||||
)?;
|
||||
// A plugin-shaped alias must use the canonical plugin's identity and version.
|
||||
let alias = executor_home.path().join(format!(
|
||||
"plugins/cache/openai-curated-remote/apparent-plugin/{version}"
|
||||
));
|
||||
std::fs::create_dir_all(alias.parent().unwrap())?;
|
||||
std::os::unix::fs::symlink(&root, &alias)?;
|
||||
let alias_script = alias.join("scripts/run.sh");
|
||||
let command = shlex::try_join(["/bin/sh", alias_script.to_string_lossy().as_ref()])?;
|
||||
responses.push(sse(vec![
|
||||
ev_response_created(call_id),
|
||||
ev_function_call(
|
||||
call_id,
|
||||
"exec_command",
|
||||
&serde_json::json!({
|
||||
"cmd": command,
|
||||
"login": false,
|
||||
"environment_id": codex_exec_server::REMOTE_ENVIRONMENT_ID,
|
||||
"yield_time_ms": 1000,
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
ev_completed(call_id),
|
||||
]));
|
||||
}
|
||||
responses.push(sse(vec![
|
||||
ev_response_created("done"),
|
||||
ev_assistant_message("done-message", "done"),
|
||||
ev_completed("done"),
|
||||
]));
|
||||
let response_mock = mount_sse_sequence(&server, responses).await;
|
||||
let base_url = server.uri();
|
||||
let mut builder = test_codex()
|
||||
.with_exec_server_url(executor.websocket_url.clone())
|
||||
.with_home(home)
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_model("gpt-5.2")
|
||||
.with_config(move |config| {
|
||||
config.chatgpt_base_url = base_url;
|
||||
config.analytics_enabled = Some(true);
|
||||
config
|
||||
.features
|
||||
.enable(Feature::SkipHostSkillDiscovery)
|
||||
.unwrap();
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
let manager = test.thread_manager.environment_manager();
|
||||
let remote = manager.default_environment().unwrap();
|
||||
tokio::time::timeout(Duration::from_secs(30), remote.wait_until_ready()).await??;
|
||||
assert!(remote.is_remote());
|
||||
let (sandbox_policy, permission_profile) =
|
||||
turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path());
|
||||
test.codex
|
||||
.start_or_steer_turn(
|
||||
TurnInputRequest::user_input(vec![UserInput::Text {
|
||||
text: "run both plugin versions".into(),
|
||||
text_elements: Vec::new(),
|
||||
}])
|
||||
.with_thread_settings(ThreadSettingsOverrides {
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
sandbox_policy: Some(sandbox_policy),
|
||||
permission_profile,
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
for call_id in ["wrong-version", "matching-version"] {
|
||||
let expected = (Some(REMOTE_PLUGIN_CONFIG_NAME), Some("scripts/run.sh"));
|
||||
let begin = wait_for_event_match(&test.codex, |event| match event {
|
||||
EventMsg::ExecCommandBegin(event) if event.call_id == call_id => Some(event.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
let end = wait_for_event_match(&test.codex, |event| match event {
|
||||
EventMsg::ExecCommandEnd(event) if event.call_id == call_id => Some(event.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
assert_eq!(end.exit_code, 0, "{}", end.aggregated_output);
|
||||
assert_eq!(
|
||||
(begin.plugin_id.as_deref(), begin.script_path.as_deref()),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
(end.plugin_id.as_deref(), end.script_path.as_deref()),
|
||||
expected
|
||||
);
|
||||
}
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
assert!(
|
||||
response_mock
|
||||
.function_call_output_text("wrong-version")
|
||||
.unwrap()
|
||||
.contains("no metrics sidecar")
|
||||
);
|
||||
let event = wait_for_analytics_event(&server, "codex_plugin_measurement_event").await;
|
||||
assert_eq!(
|
||||
serde_json::json!({
|
||||
"plugin_id": event["event_params"]["plugin_id"],
|
||||
"operation": event["event_params"]["operation"],
|
||||
"measurement_name": event["event_params"]["measurement_name"],
|
||||
"number_value": event["event_params"]["number_value"],
|
||||
"dimensions": event["event_params"]["dimensions"],
|
||||
"thread_id": event["event_params"]["thread_id"],
|
||||
"item_id": event["event_params"]["item_id"],
|
||||
}),
|
||||
serde_json::json!({
|
||||
"plugin_id": REMOTE_PLUGIN_CONFIG_NAME,
|
||||
"operation": "dependency_install",
|
||||
"measurement_name": "duration_ms",
|
||||
"number_value": 7.0,
|
||||
"dimensions": {"release": "v1"},
|
||||
"thread_id": test.session_configured.thread_id.to_string(),
|
||||
"item_id": "matching-version",
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn thread_disabled_plugins_filter_skills_and_tools_without_changing_shared_plugins()
|
||||
-> Result<()> {
|
||||
|
||||
@@ -8,4 +8,5 @@ codex_rust_crate(
|
||||
"src/assets/success_legacy.html",
|
||||
],
|
||||
crate_name = "codex_login",
|
||||
unit_test_timeout = "long",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user