mirror of
https://github.com/openai/codex.git
synced 2026-05-15 00:32:51 +00:00
Compare commits
5 Commits
starr/exec
...
codex/viya
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
106296cb8d | ||
|
|
641f65a858 | ||
|
|
5e4c73cd78 | ||
|
|
4825e48fcd | ||
|
|
45c47d2b33 |
@@ -1275,6 +1275,7 @@ impl PluginRequestProcessor {
|
||||
| MarketplaceError::PluginNotFound { .. }
|
||||
| MarketplaceError::PluginNotAvailable { .. }
|
||||
| MarketplaceError::PluginsDisabled
|
||||
| MarketplaceError::MarketplaceBlocked { .. }
|
||||
| MarketplaceError::InvalidPlugin(_) => invalid_request(err.to_string()),
|
||||
MarketplaceError::Io { .. } => internal_error(format!("failed to {action}: {err}")),
|
||||
}
|
||||
|
||||
@@ -90,6 +90,8 @@ pub struct ConfigRequirements {
|
||||
pub managed_hooks: Option<ConstrainedWithSource<ManagedHooksRequirementsToml>>,
|
||||
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
|
||||
pub plugins: Option<Sourced<BTreeMap<String, PluginRequirementsToml>>>,
|
||||
pub skills: Option<Sourced<SkillsRequirementsToml>>,
|
||||
pub plugin_marketplaces: Option<Sourced<PluginMarketplaceRequirementsToml>>,
|
||||
pub exec_policy: Option<Sourced<RequirementsExecPolicy>>,
|
||||
pub enforce_residency: ConstrainedWithSource<Option<ResidencyRequirement>>,
|
||||
/// Managed network constraints derived from requirements.
|
||||
@@ -123,6 +125,8 @@ impl Default for ConfigRequirements {
|
||||
managed_hooks: None,
|
||||
mcp_servers: None,
|
||||
plugins: None,
|
||||
skills: None,
|
||||
plugin_marketplaces: None,
|
||||
exec_policy: None,
|
||||
enforce_residency: ConstrainedWithSource::new(
|
||||
Constrained::allow_any(/*initial_value*/ None),
|
||||
@@ -164,6 +168,57 @@ impl PluginRequirementsToml {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SkillSourceRequirement {
|
||||
User,
|
||||
Repo,
|
||||
System,
|
||||
Admin,
|
||||
Plugin,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct SkillsRequirementsToml {
|
||||
pub allowed_sources: Option<Vec<SkillSourceRequirement>>,
|
||||
}
|
||||
|
||||
impl SkillsRequirementsToml {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.allowed_sources.is_none()
|
||||
}
|
||||
|
||||
pub fn allows_source(&self, source: SkillSourceRequirement) -> bool {
|
||||
self.allowed_sources
|
||||
.as_ref()
|
||||
.is_none_or(|sources| sources.contains(&source))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct PluginMarketplaceRequirementsToml {
|
||||
pub allowed_names: Option<Vec<String>>,
|
||||
pub allow_user_additions: Option<bool>,
|
||||
}
|
||||
|
||||
impl PluginMarketplaceRequirementsToml {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.allowed_names.is_none() && self.allow_user_additions.is_none()
|
||||
}
|
||||
|
||||
pub fn allows_marketplace(&self, marketplace_name: &str) -> bool {
|
||||
self.allowed_names.as_ref().is_none_or(|allowed_names| {
|
||||
allowed_names
|
||||
.iter()
|
||||
.any(|allowed_name| allowed_name == marketplace_name)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn allows_user_additions(&self) -> bool {
|
||||
self.allow_user_additions.unwrap_or(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct NetworkDomainPermissionsToml {
|
||||
#[serde(flatten)]
|
||||
@@ -694,6 +749,8 @@ pub struct ConfigRequirementsWithSources {
|
||||
pub hooks: Option<Sourced<ManagedHooksRequirementsToml>>,
|
||||
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
|
||||
pub plugins: Option<Sourced<BTreeMap<String, PluginRequirementsToml>>>,
|
||||
pub skills: Option<Sourced<SkillsRequirementsToml>>,
|
||||
pub plugin_marketplaces: Option<Sourced<PluginMarketplaceRequirementsToml>>,
|
||||
pub apps: Option<Sourced<AppsRequirementsToml>>,
|
||||
pub rules: Option<Sourced<RequirementsExecPolicyToml>>,
|
||||
pub enforce_residency: Option<Sourced<ResidencyRequirement>>,
|
||||
@@ -786,6 +843,8 @@ impl ConfigRequirementsWithSources {
|
||||
hooks,
|
||||
mcp_servers,
|
||||
plugins,
|
||||
skills: _,
|
||||
plugin_marketplaces: _,
|
||||
apps,
|
||||
rules,
|
||||
enforce_residency,
|
||||
@@ -923,6 +982,8 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
hooks,
|
||||
mcp_servers,
|
||||
plugins,
|
||||
skills,
|
||||
plugin_marketplaces,
|
||||
apps: _apps,
|
||||
rules,
|
||||
enforce_residency,
|
||||
@@ -1158,6 +1219,8 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
managed_hooks,
|
||||
mcp_servers,
|
||||
plugins,
|
||||
skills,
|
||||
plugin_marketplaces,
|
||||
exec_policy,
|
||||
enforce_residency,
|
||||
network,
|
||||
@@ -1251,6 +1314,8 @@ mod tests {
|
||||
hooks: hooks.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
mcp_servers: mcp_servers.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
plugins: plugins.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
skills: None,
|
||||
plugin_marketplaces: None,
|
||||
apps: apps.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
rules: rules.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
enforce_residency: enforce_residency
|
||||
@@ -1330,6 +1395,8 @@ mod tests {
|
||||
hooks: None,
|
||||
mcp_servers: None,
|
||||
plugins: None,
|
||||
skills: None,
|
||||
plugin_marketplaces: None,
|
||||
apps: None,
|
||||
rules: None,
|
||||
enforce_residency: Some(Sourced::new(enforce_residency, enforce_source)),
|
||||
@@ -1369,6 +1436,8 @@ mod tests {
|
||||
hooks: None,
|
||||
mcp_servers: None,
|
||||
plugins: None,
|
||||
skills: None,
|
||||
plugin_marketplaces: None,
|
||||
apps: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
@@ -1416,6 +1485,8 @@ mod tests {
|
||||
hooks: None,
|
||||
mcp_servers: None,
|
||||
plugins: None,
|
||||
skills: None,
|
||||
plugin_marketplaces: None,
|
||||
apps: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
|
||||
@@ -49,11 +49,14 @@ pub use config_requirements::NetworkDomainPermissionsToml;
|
||||
pub use config_requirements::NetworkRequirementsToml;
|
||||
pub use config_requirements::NetworkUnixSocketPermissionToml;
|
||||
pub use config_requirements::NetworkUnixSocketPermissionsToml;
|
||||
pub use config_requirements::PluginMarketplaceRequirementsToml;
|
||||
pub use config_requirements::PluginRequirementsToml;
|
||||
pub use config_requirements::RemoteSandboxConfigToml;
|
||||
pub use config_requirements::RequirementSource;
|
||||
pub use config_requirements::ResidencyRequirement;
|
||||
pub use config_requirements::SandboxModeRequirement;
|
||||
pub use config_requirements::SkillSourceRequirement;
|
||||
pub use config_requirements::SkillsRequirementsToml;
|
||||
pub use config_requirements::Sourced;
|
||||
pub use config_requirements::WebSearchModeRequirement;
|
||||
pub use config_requirements::sandbox_mode_requirement_for_permission_profile;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::OPENAI_CURATED_MARKETPLACE_NAME;
|
||||
use crate::manager::configured_plugins_for_stack;
|
||||
use crate::manifest::PluginManifestHooks;
|
||||
use crate::manifest::PluginManifestPaths;
|
||||
use crate::manifest::load_plugin_manifest;
|
||||
@@ -378,10 +379,7 @@ fn refresh_non_curated_plugin_cache_with_mode(
|
||||
fn configured_plugins_from_stack(
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
) -> HashMap<String, PluginConfig> {
|
||||
let Some(user_layer) = config_layer_stack.get_user_layer() else {
|
||||
return HashMap::new();
|
||||
};
|
||||
configured_plugins_from_user_config_value(&user_layer.config)
|
||||
configured_plugins_for_stack(config_layer_stack)
|
||||
}
|
||||
|
||||
fn is_full_git_sha(value: &str) -> bool {
|
||||
|
||||
@@ -48,6 +48,7 @@ use crate::store::PluginStoreError;
|
||||
use codex_analytics::AnalyticsEventsClient;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::PluginConfigEdit;
|
||||
use codex_config::PluginMarketplaceRequirementsToml;
|
||||
use codex_config::apply_user_plugin_config_edits;
|
||||
use codex_config::clear_user_plugin;
|
||||
use codex_config::set_user_plugin_enabled;
|
||||
@@ -401,6 +402,7 @@ pub struct PluginsManager {
|
||||
struct CachedPluginLoadOutcome {
|
||||
config_version: String,
|
||||
plugin_hooks_enabled: bool,
|
||||
plugin_marketplace_requirements: Option<PluginMarketplaceRequirementsToml>,
|
||||
outcome: PluginLoadOutcome,
|
||||
}
|
||||
|
||||
@@ -473,9 +475,13 @@ impl PluginsManager {
|
||||
|
||||
let plugin_hooks_enabled = config.plugin_hooks_enabled;
|
||||
let config_version = version_for_toml(&config.config_layer_stack.effective_config());
|
||||
let plugin_marketplace_requirements = plugin_marketplace_requirements(config);
|
||||
if !force_reload
|
||||
&& let Some(outcome) =
|
||||
self.cached_enabled_outcome(&config_version, plugin_hooks_enabled)
|
||||
&& let Some(outcome) = self.cached_enabled_outcome(
|
||||
&config_version,
|
||||
plugin_hooks_enabled,
|
||||
plugin_marketplace_requirements.as_ref(),
|
||||
)
|
||||
{
|
||||
return outcome;
|
||||
}
|
||||
@@ -496,6 +502,7 @@ impl PluginsManager {
|
||||
*cache = Some(CachedPluginLoadOutcome {
|
||||
config_version,
|
||||
plugin_hooks_enabled,
|
||||
plugin_marketplace_requirements,
|
||||
outcome: outcome.clone(),
|
||||
});
|
||||
outcome
|
||||
@@ -553,6 +560,7 @@ impl PluginsManager {
|
||||
&self,
|
||||
config_version: &str,
|
||||
plugin_hooks_enabled: bool,
|
||||
plugin_marketplace_requirements: Option<&PluginMarketplaceRequirementsToml>,
|
||||
) -> Option<PluginLoadOutcome> {
|
||||
match self.cached_enabled_outcome.read() {
|
||||
Ok(cache) => cache
|
||||
@@ -560,6 +568,8 @@ impl PluginsManager {
|
||||
.filter(|cached| {
|
||||
cached.config_version == config_version
|
||||
&& cached.plugin_hooks_enabled == plugin_hooks_enabled
|
||||
&& cached.plugin_marketplace_requirements.as_ref()
|
||||
== plugin_marketplace_requirements
|
||||
})
|
||||
.map(|cached| cached.outcome.clone()),
|
||||
Err(err) => err
|
||||
@@ -568,6 +578,8 @@ impl PluginsManager {
|
||||
.filter(|cached| {
|
||||
cached.config_version == config_version
|
||||
&& cached.plugin_hooks_enabled == plugin_hooks_enabled
|
||||
&& cached.plugin_marketplace_requirements.as_ref()
|
||||
== plugin_marketplace_requirements
|
||||
})
|
||||
.map(|cached| cached.outcome.clone()),
|
||||
}
|
||||
@@ -589,7 +601,16 @@ impl PluginsManager {
|
||||
return HashMap::new();
|
||||
};
|
||||
|
||||
remote_installed_plugins_to_config(plugins, &self.store)
|
||||
if plugin_marketplace_requirements(config).is_none() {
|
||||
return remote_installed_plugins_to_config(plugins, &self.store);
|
||||
}
|
||||
|
||||
let plugins = plugins
|
||||
.iter()
|
||||
.filter(|plugin| marketplace_is_allowed(config, &plugin.marketplace_name))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
remote_installed_plugins_to_config(&plugins, &self.store)
|
||||
}
|
||||
|
||||
fn write_remote_installed_plugins_cache(&self, plugins: Vec<RemoteInstalledPlugin>) -> bool {
|
||||
@@ -801,6 +822,25 @@ impl PluginsManager {
|
||||
self.install_resolved_plugin(resolved).await
|
||||
}
|
||||
|
||||
pub async fn install_plugin_for_config(
|
||||
&self,
|
||||
config: &PluginsConfigInput,
|
||||
request: PluginInstallRequest,
|
||||
) -> Result<PluginInstallOutcome, PluginInstallError> {
|
||||
let resolved = find_installable_marketplace_plugin(
|
||||
&request.marketplace_path,
|
||||
&request.plugin_name,
|
||||
self.restriction_product,
|
||||
)?;
|
||||
if !marketplace_is_allowed(config, &resolved.plugin_id.marketplace_name) {
|
||||
return Err(MarketplaceError::MarketplaceBlocked {
|
||||
marketplace_name: resolved.plugin_id.marketplace_name,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
self.install_resolved_plugin(resolved).await
|
||||
}
|
||||
|
||||
pub async fn install_plugin_with_remote_sync(
|
||||
&self,
|
||||
config: &PluginsConfigInput,
|
||||
@@ -952,6 +992,9 @@ impl PluginsManager {
|
||||
if !config.plugins_enabled {
|
||||
return Ok(RemotePluginSyncResult::default());
|
||||
}
|
||||
if !marketplace_is_allowed(config, OPENAI_CURATED_MARKETPLACE_NAME) {
|
||||
return Ok(RemotePluginSyncResult::default());
|
||||
}
|
||||
|
||||
info!("starting remote plugin sync");
|
||||
let remote_plugins = crate::remote_legacy::fetch_remote_plugin_status(
|
||||
@@ -960,7 +1003,7 @@ impl PluginsManager {
|
||||
)
|
||||
.await
|
||||
.map_err(PluginRemoteSyncError::from)?;
|
||||
let configured_plugins = configured_plugins_from_stack(&config.config_layer_stack);
|
||||
let configured_plugins = configured_plugins_for_config(config);
|
||||
let curated_marketplace_root = curated_plugins_repo_path(self.codex_home.as_path());
|
||||
let curated_marketplace_path = AbsolutePathBuf::try_from(
|
||||
curated_marketplace_root.join(".agents/plugins/marketplace.json"),
|
||||
@@ -1174,6 +1217,9 @@ impl PluginsManager {
|
||||
.marketplaces
|
||||
.into_iter()
|
||||
.filter_map(|marketplace| {
|
||||
if !marketplace_is_allowed(config, &marketplace.name) {
|
||||
return None;
|
||||
}
|
||||
let marketplace_name = marketplace.name.clone();
|
||||
let plugins = marketplace
|
||||
.plugins
|
||||
@@ -1228,6 +1274,11 @@ impl PluginsManager {
|
||||
}
|
||||
|
||||
let plugin = find_marketplace_plugin(&request.marketplace_path, &request.plugin_name)?;
|
||||
if !marketplace_is_allowed(config, &plugin.plugin_id.marketplace_name) {
|
||||
return Err(MarketplaceError::MarketplaceBlocked {
|
||||
marketplace_name: plugin.plugin_id.marketplace_name,
|
||||
});
|
||||
}
|
||||
if !self.restriction_product_matches(plugin.policy.products.as_deref()) {
|
||||
return Err(MarketplaceError::PluginNotFound {
|
||||
plugin_name: plugin.plugin_id.plugin_name,
|
||||
@@ -1491,8 +1542,10 @@ impl PluginsManager {
|
||||
config: &PluginsConfigInput,
|
||||
marketplace_name: Option<&str>,
|
||||
) -> Result<ConfiguredMarketplaceUpgradeOutcome, String> {
|
||||
let configured_marketplace_names =
|
||||
configured_git_marketplace_names(&config.config_layer_stack);
|
||||
if let Some(marketplace_name) = marketplace_name
|
||||
&& !configured_git_marketplace_names(&config.config_layer_stack)
|
||||
&& !configured_marketplace_names
|
||||
.iter()
|
||||
.any(|name| name == marketplace_name)
|
||||
{
|
||||
@@ -1501,11 +1554,49 @@ impl PluginsManager {
|
||||
));
|
||||
}
|
||||
|
||||
let mut outcome = upgrade_configured_git_marketplaces(
|
||||
self.codex_home.as_path(),
|
||||
&config.config_layer_stack,
|
||||
marketplace_name,
|
||||
);
|
||||
let mut outcome = if config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.plugin_marketplaces
|
||||
.is_none()
|
||||
{
|
||||
upgrade_configured_git_marketplaces(
|
||||
self.codex_home.as_path(),
|
||||
&config.config_layer_stack,
|
||||
marketplace_name,
|
||||
)
|
||||
} else {
|
||||
let selected_marketplace_names = match marketplace_name {
|
||||
Some(marketplace_name) => {
|
||||
if !marketplace_is_allowed(config, marketplace_name) {
|
||||
return Err(format!(
|
||||
"marketplace `{marketplace_name}` is not allowed by managed requirements"
|
||||
));
|
||||
}
|
||||
vec![marketplace_name.to_string()]
|
||||
}
|
||||
None => configured_marketplace_names
|
||||
.into_iter()
|
||||
.filter(|marketplace_name| marketplace_is_allowed(config, marketplace_name))
|
||||
.collect::<Vec<_>>(),
|
||||
};
|
||||
let mut outcome = ConfiguredMarketplaceUpgradeOutcome::default();
|
||||
for marketplace_name in selected_marketplace_names {
|
||||
let mut marketplace_outcome = upgrade_configured_git_marketplaces(
|
||||
self.codex_home.as_path(),
|
||||
&config.config_layer_stack,
|
||||
Some(&marketplace_name),
|
||||
);
|
||||
outcome
|
||||
.selected_marketplaces
|
||||
.append(&mut marketplace_outcome.selected_marketplaces);
|
||||
outcome
|
||||
.upgraded_roots
|
||||
.append(&mut marketplace_outcome.upgraded_roots);
|
||||
outcome.errors.append(&mut marketplace_outcome.errors);
|
||||
}
|
||||
outcome
|
||||
};
|
||||
if !outcome.upgraded_roots.is_empty() {
|
||||
match refresh_non_curated_plugin_cache_force_reinstall(
|
||||
self.codex_home.as_path(),
|
||||
@@ -1813,7 +1904,7 @@ impl PluginsManager {
|
||||
&self,
|
||||
config: &PluginsConfigInput,
|
||||
) -> (HashSet<String>, HashSet<String>) {
|
||||
let configured_plugins = configured_plugins_from_stack(&config.config_layer_stack);
|
||||
let configured_plugins = configured_plugins_for_config(config);
|
||||
let installed_plugins = configured_plugins
|
||||
.keys()
|
||||
.filter(|plugin_key| {
|
||||
@@ -1913,6 +2004,7 @@ impl PluginInstallError {
|
||||
| MarketplaceError::InvalidMarketplaceFile { .. }
|
||||
| MarketplaceError::PluginNotFound { .. }
|
||||
| MarketplaceError::PluginNotAvailable { .. }
|
||||
| MarketplaceError::MarketplaceBlocked { .. }
|
||||
| MarketplaceError::InvalidPlugin(_)
|
||||
) | Self::Store(PluginStoreError::Invalid(_))
|
||||
)
|
||||
@@ -1957,6 +2049,60 @@ pub(crate) fn configured_plugins_from_stack(
|
||||
configured_plugins_from_user_config_value(&user_layer.config)
|
||||
}
|
||||
|
||||
pub(crate) fn configured_plugins_for_stack(
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
) -> HashMap<String, PluginConfig> {
|
||||
let configured_plugins = configured_plugins_from_stack(config_layer_stack);
|
||||
if config_layer_stack
|
||||
.requirements()
|
||||
.plugin_marketplaces
|
||||
.is_none()
|
||||
{
|
||||
return configured_plugins;
|
||||
}
|
||||
|
||||
configured_plugins
|
||||
.into_iter()
|
||||
.filter(|(plugin_key, _)| plugin_key_is_allowed(config_layer_stack, plugin_key))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn marketplace_is_allowed(config: &PluginsConfigInput, marketplace_name: &str) -> bool {
|
||||
marketplace_is_allowed_in_stack(&config.config_layer_stack, marketplace_name)
|
||||
}
|
||||
|
||||
fn plugin_marketplace_requirements(
|
||||
config: &PluginsConfigInput,
|
||||
) -> Option<PluginMarketplaceRequirementsToml> {
|
||||
config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.plugin_marketplaces
|
||||
.as_ref()
|
||||
.map(|requirements| requirements.value.clone())
|
||||
}
|
||||
|
||||
pub(crate) fn marketplace_is_allowed_in_stack(
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
marketplace_name: &str,
|
||||
) -> bool {
|
||||
config_layer_stack
|
||||
.requirements()
|
||||
.plugin_marketplaces
|
||||
.as_ref()
|
||||
.is_none_or(|requirements| requirements.value.allows_marketplace(marketplace_name))
|
||||
}
|
||||
|
||||
fn plugin_key_is_allowed(config_layer_stack: &ConfigLayerStack, plugin_key: &str) -> bool {
|
||||
PluginId::parse(plugin_key).ok().is_some_and(|plugin_id| {
|
||||
marketplace_is_allowed_in_stack(config_layer_stack, &plugin_id.marketplace_name)
|
||||
})
|
||||
}
|
||||
|
||||
fn configured_plugins_for_config(config: &PluginsConfigInput) -> HashMap<String, PluginConfig> {
|
||||
configured_plugins_for_stack(&config.config_layer_stack)
|
||||
}
|
||||
|
||||
fn configured_plugins_from_user_config_value(
|
||||
user_config: &toml::Value,
|
||||
) -> HashMap<String, PluginConfig> {
|
||||
|
||||
@@ -23,6 +23,9 @@ use codex_config::ConfigRequirements;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::McpServerToolConfig;
|
||||
use codex_config::PluginMarketplaceRequirementsToml;
|
||||
use codex_config::RequirementSource;
|
||||
use codex_config::Sourced;
|
||||
use codex_config::types::McpServerTransportConfig;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::protocol::Product;
|
||||
@@ -141,6 +144,32 @@ async fn load_plugins_from_config(config_toml: &str, codex_home: &Path) -> Plugi
|
||||
.await
|
||||
}
|
||||
|
||||
fn plugins_config_with_requirements(
|
||||
codex_home: &Path,
|
||||
user_config: Value,
|
||||
requirements: ConfigRequirements,
|
||||
) -> PluginsConfigInput {
|
||||
let config_path =
|
||||
codex_utils_absolute_path::AbsolutePathBuf::try_from(codex_home.join(CONFIG_TOML_FILE))
|
||||
.expect("config path should be absolute");
|
||||
let config_layer_stack = ConfigLayerStack::new(
|
||||
vec![ConfigLayerEntry::new(
|
||||
ConfigLayerSource::User { file: config_path },
|
||||
user_config,
|
||||
)],
|
||||
requirements,
|
||||
ConfigRequirementsToml::default(),
|
||||
)
|
||||
.expect("valid config layer stack");
|
||||
PluginsConfigInput::new(
|
||||
config_layer_stack,
|
||||
/*plugins_enabled*/ true,
|
||||
/*remote_plugin_enabled*/ false,
|
||||
/*plugin_hooks_enabled*/ false,
|
||||
"https://chatgpt.com/backend-api/".to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn load_config(codex_home: &Path, cwd: &Path) -> PluginsConfigInput {
|
||||
load_plugins_config_input(codex_home, cwd).await
|
||||
}
|
||||
@@ -1576,6 +1605,143 @@ enabled = false
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn managed_marketplace_allowlist_hides_and_disables_unapproved_plugins() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let repo_root = tmp.path().join("repo");
|
||||
fs::create_dir_all(repo_root.join(".git")).unwrap();
|
||||
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
|
||||
write_plugin(
|
||||
&tmp.path().join("plugins/cache/debug"),
|
||||
"sample/local",
|
||||
"sample",
|
||||
);
|
||||
fs::write(
|
||||
repo_root.join(".agents/plugins/marketplace.json"),
|
||||
r#"{
|
||||
"name": "debug",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "sample",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./sample"
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let config = plugins_config_with_requirements(
|
||||
tmp.path(),
|
||||
toml::toml! {
|
||||
[plugins."sample@debug"]
|
||||
enabled = true
|
||||
}
|
||||
.into(),
|
||||
ConfigRequirements {
|
||||
plugin_marketplaces: Some(Sourced::new(
|
||||
PluginMarketplaceRequirementsToml {
|
||||
allowed_names: Some(vec!["openai-curated".to_string()]),
|
||||
allow_user_additions: Some(false),
|
||||
},
|
||||
RequirementSource::Unknown,
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
|
||||
let outcome = manager.plugins_for_config(&config).await;
|
||||
assert_eq!(outcome.plugins(), &[]);
|
||||
|
||||
let marketplaces = manager
|
||||
.list_marketplaces_for_config(&config, &[AbsolutePathBuf::try_from(repo_root).unwrap()])
|
||||
.unwrap()
|
||||
.marketplaces;
|
||||
assert_eq!(marketplaces, Vec::new());
|
||||
|
||||
let err = manager
|
||||
.read_plugin_for_config(
|
||||
&config,
|
||||
&PluginReadRequest {
|
||||
plugin_name: "sample".to_string(),
|
||||
marketplace_path: AbsolutePathBuf::try_from(
|
||||
tmp.path().join("repo/.agents/plugins/marketplace.json"),
|
||||
)
|
||||
.unwrap(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect_err("disallowed marketplace should not be readable");
|
||||
assert!(matches!(
|
||||
err,
|
||||
MarketplaceError::MarketplaceBlocked { marketplace_name }
|
||||
if marketplace_name == "debug"
|
||||
));
|
||||
|
||||
let err = manager
|
||||
.install_plugin_for_config(
|
||||
&config,
|
||||
PluginInstallRequest {
|
||||
plugin_name: "sample".to_string(),
|
||||
marketplace_path: AbsolutePathBuf::try_from(
|
||||
tmp.path().join("repo/.agents/plugins/marketplace.json"),
|
||||
)
|
||||
.unwrap(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect_err("disallowed marketplace should not be installable");
|
||||
assert!(matches!(
|
||||
err,
|
||||
PluginInstallError::Marketplace(MarketplaceError::MarketplaceBlocked {
|
||||
marketplace_name
|
||||
}) if marketplace_name == "debug"
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_for_config_separates_cache_entries_by_managed_marketplace_allowlist() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
write_plugin(
|
||||
&tmp.path().join("plugins/cache/debug"),
|
||||
"sample/local",
|
||||
"sample",
|
||||
);
|
||||
let user_config: Value = toml::toml! {
|
||||
[plugins."sample@debug"]
|
||||
enabled = true
|
||||
}
|
||||
.into();
|
||||
let unrestricted = plugins_config_with_requirements(
|
||||
tmp.path(),
|
||||
user_config.clone(),
|
||||
ConfigRequirements::default(),
|
||||
);
|
||||
let restricted = plugins_config_with_requirements(
|
||||
tmp.path(),
|
||||
user_config,
|
||||
ConfigRequirements {
|
||||
plugin_marketplaces: Some(Sourced::new(
|
||||
PluginMarketplaceRequirementsToml {
|
||||
allowed_names: Some(vec!["openai-curated".to_string()]),
|
||||
allow_user_additions: Some(false),
|
||||
},
|
||||
RequirementSource::Unknown,
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
|
||||
let unrestricted = manager.plugins_for_config(&unrestricted).await;
|
||||
assert_eq!(unrestricted.plugins().len(), 1);
|
||||
|
||||
let restricted = manager.plugins_for_config(&restricted).await;
|
||||
assert_eq!(restricted.plugins(), &[]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_marketplaces_returns_empty_when_feature_disabled() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
@@ -2827,6 +2993,65 @@ enabled = false
|
||||
assert!(config.contains("enabled = false"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sync_plugins_from_remote_skips_disallowed_curated_marketplace() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let curated_root = curated_plugins_repo_path(tmp.path());
|
||||
write_openai_curated_marketplace(&curated_root, &["gmail"]);
|
||||
write_curated_plugin_sha(tmp.path(), TEST_CURATED_PLUGIN_SHA);
|
||||
write_file(
|
||||
&tmp.path().join(CONFIG_TOML_FILE),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
"#,
|
||||
);
|
||||
|
||||
let mut config = load_config(tmp.path(), tmp.path()).await;
|
||||
config.config_layer_stack = ConfigLayerStack::new(
|
||||
config
|
||||
.config_layer_stack
|
||||
.get_layers(
|
||||
codex_config::ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ true,
|
||||
)
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.collect(),
|
||||
ConfigRequirements {
|
||||
plugin_marketplaces: Some(Sourced::new(
|
||||
PluginMarketplaceRequirementsToml {
|
||||
allowed_names: Some(vec!["arm-internal".to_string()]),
|
||||
allow_user_additions: Some(false),
|
||||
},
|
||||
RequirementSource::Unknown,
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigRequirementsToml::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
let result = manager
|
||||
.sync_plugins_from_remote(
|
||||
&config,
|
||||
Some(&CodexAuth::create_dummy_chatgpt_auth_for_testing()),
|
||||
/*additive_only*/ false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result, RemotePluginSyncResult::default());
|
||||
assert!(
|
||||
!tmp.path()
|
||||
.join("plugins/cache/openai-curated/gmail")
|
||||
.exists()
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).unwrap(),
|
||||
"[features]\nplugins = true\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sync_plugins_from_remote_uses_first_duplicate_local_plugin_entry() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -158,6 +158,9 @@ pub enum MarketplaceError {
|
||||
#[error("plugins feature is disabled")]
|
||||
PluginsDisabled,
|
||||
|
||||
#[error("marketplace `{marketplace_name}` is not allowed by managed requirements")]
|
||||
MarketplaceBlocked { marketplace_name: String },
|
||||
|
||||
#[error("{0}")]
|
||||
InvalidPlugin(String),
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::SkillSourceRequirement;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_protocol::protocol::Product;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
@@ -51,7 +52,7 @@ impl SkillsLoadInput {
|
||||
pub struct SkillsManager {
|
||||
codex_home: AbsolutePathBuf,
|
||||
restriction_product: Option<Product>,
|
||||
cache_by_cwd: RwLock<HashMap<AbsolutePathBuf, SkillLoadOutcome>>,
|
||||
cache_by_cwd: RwLock<HashMap<CwdSkillsCacheKey, SkillLoadOutcome>>,
|
||||
cache_by_config: RwLock<HashMap<ConfigSkillsCacheKey, SkillLoadOutcome>>,
|
||||
}
|
||||
|
||||
@@ -123,6 +124,7 @@ impl SkillsManager {
|
||||
if !input.bundled_skills_enabled {
|
||||
roots.retain(|root| root.scope != SkillScope::System);
|
||||
}
|
||||
retain_allowed_skill_roots(&mut roots, &input.config_layer_stack);
|
||||
roots
|
||||
}
|
||||
|
||||
@@ -144,9 +146,10 @@ impl SkillsManager {
|
||||
fs: Option<Arc<dyn ExecutorFileSystem>>,
|
||||
) -> SkillLoadOutcome {
|
||||
let use_cwd_cache = fs.is_some();
|
||||
let cache_key = cwd_skills_cache_key(&input.cwd, &input.config_layer_stack);
|
||||
if use_cwd_cache
|
||||
&& !force_reload
|
||||
&& let Some(outcome) = self.cached_outcome_for_cwd(&input.cwd)
|
||||
&& let Some(outcome) = self.cached_outcome_for_cwd(&cache_key)
|
||||
{
|
||||
return outcome;
|
||||
}
|
||||
@@ -173,6 +176,7 @@ impl SkillsManager {
|
||||
}),
|
||||
);
|
||||
}
|
||||
retain_allowed_skill_roots(&mut roots, &input.config_layer_stack);
|
||||
let skill_config_rules = skill_config_rules_from_stack(&input.config_layer_stack);
|
||||
let outcome = self.build_skill_outcome(roots, &skill_config_rules).await;
|
||||
if use_cwd_cache {
|
||||
@@ -180,7 +184,7 @@ impl SkillsManager {
|
||||
.cache_by_cwd
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
cache.insert(input.cwd.clone(), outcome.clone());
|
||||
cache.insert(cache_key, outcome.clone());
|
||||
}
|
||||
outcome
|
||||
}
|
||||
@@ -221,10 +225,10 @@ impl SkillsManager {
|
||||
info!("skills cache cleared ({cleared} entries)");
|
||||
}
|
||||
|
||||
fn cached_outcome_for_cwd(&self, cwd: &AbsolutePathBuf) -> Option<SkillLoadOutcome> {
|
||||
fn cached_outcome_for_cwd(&self, cache_key: &CwdSkillsCacheKey) -> Option<SkillLoadOutcome> {
|
||||
match self.cache_by_cwd.read() {
|
||||
Ok(cache) => cache.get(cwd).cloned(),
|
||||
Err(err) => err.into_inner().get(cwd).cloned(),
|
||||
Ok(cache) => cache.get(cache_key).cloned(),
|
||||
Err(err) => err.into_inner().get(cache_key).cloned(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,12 +243,43 @@ impl SkillsManager {
|
||||
}
|
||||
}
|
||||
|
||||
fn retain_allowed_skill_roots(roots: &mut Vec<SkillRoot>, config_layer_stack: &ConfigLayerStack) {
|
||||
let Some(requirements) = config_layer_stack.requirements().skills.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
roots.retain(|root| {
|
||||
requirements
|
||||
.value
|
||||
.allows_source(skill_source_for_root(root))
|
||||
});
|
||||
}
|
||||
|
||||
fn skill_source_for_root(root: &SkillRoot) -> SkillSourceRequirement {
|
||||
if root.plugin_id.is_some() {
|
||||
return SkillSourceRequirement::Plugin;
|
||||
}
|
||||
|
||||
match root.scope {
|
||||
SkillScope::Repo => SkillSourceRequirement::Repo,
|
||||
SkillScope::User => SkillSourceRequirement::User,
|
||||
SkillScope::System => SkillSourceRequirement::System,
|
||||
SkillScope::Admin => SkillSourceRequirement::Admin,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
struct ConfigSkillsCacheKey {
|
||||
roots: Vec<(AbsolutePathBuf, u8, Option<String>)>,
|
||||
skill_config_rules: SkillConfigRules,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
struct CwdSkillsCacheKey {
|
||||
cwd: AbsolutePathBuf,
|
||||
allowed_sources: Option<Vec<SkillSourceRequirement>>,
|
||||
}
|
||||
|
||||
pub fn bundled_skills_enabled_from_stack(
|
||||
config_layer_stack: &codex_config::ConfigLayerStack,
|
||||
) -> bool {
|
||||
@@ -288,6 +323,20 @@ fn config_skills_cache_key(
|
||||
}
|
||||
}
|
||||
|
||||
fn cwd_skills_cache_key(
|
||||
cwd: &AbsolutePathBuf,
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
) -> CwdSkillsCacheKey {
|
||||
CwdSkillsCacheKey {
|
||||
cwd: cwd.clone(),
|
||||
allowed_sources: config_layer_stack
|
||||
.requirements()
|
||||
.skills
|
||||
.as_ref()
|
||||
.and_then(|requirements| requirements.value.allowed_sources.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_skill_outcome(
|
||||
mut outcome: SkillLoadOutcome,
|
||||
disabled_paths: HashSet<AbsolutePathBuf>,
|
||||
|
||||
@@ -6,7 +6,12 @@ use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::ConfigLayerEntry;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigRequirements;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::RequirementSource;
|
||||
use codex_config::SkillSourceRequirement;
|
||||
use codex_config::SkillsRequirementsToml;
|
||||
use codex_config::Sourced;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::test_support::PathBufExt;
|
||||
@@ -27,6 +32,13 @@ fn write_user_skill(codex_home: &TempDir, dir: &str, name: &str, description: &s
|
||||
fs::write(skill_dir.join("SKILL.md"), content).unwrap();
|
||||
}
|
||||
|
||||
fn write_repo_skill(cwd: &TempDir, dir: &str, name: &str, description: &str) {
|
||||
let skill_dir = cwd.path().join(".agents/skills").join(dir);
|
||||
fs::create_dir_all(&skill_dir).unwrap();
|
||||
let content = format!("---\nname: {name}\ndescription: {description}\n---\n\n# Body\n");
|
||||
fs::write(skill_dir.join("SKILL.md"), content).unwrap();
|
||||
}
|
||||
|
||||
fn write_plugin_skill(
|
||||
codex_home: &TempDir,
|
||||
marketplace: &str,
|
||||
@@ -94,9 +106,17 @@ fn user_config_layer(codex_home: &TempDir, config_toml: &str) -> ConfigLayerEntr
|
||||
}
|
||||
|
||||
fn config_stack(codex_home: &TempDir, user_config_toml: &str) -> ConfigLayerStack {
|
||||
config_stack_with_requirements(codex_home, user_config_toml, ConfigRequirements::default())
|
||||
}
|
||||
|
||||
fn config_stack_with_requirements(
|
||||
codex_home: &TempDir,
|
||||
user_config_toml: &str,
|
||||
requirements: ConfigRequirements,
|
||||
) -> ConfigLayerStack {
|
||||
ConfigLayerStack::new(
|
||||
vec![user_config_layer(codex_home, user_config_toml)],
|
||||
Default::default(),
|
||||
requirements,
|
||||
ConfigRequirementsToml::default(),
|
||||
)
|
||||
.expect("valid config layer stack")
|
||||
@@ -262,6 +282,65 @@ async fn skills_for_config_disables_plugin_skills_by_name() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn skills_for_config_filters_disallowed_managed_sources() {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
let cwd = tempfile::tempdir().expect("tempdir");
|
||||
write_user_skill(&codex_home, "user", "user-skill", "from user");
|
||||
write_repo_skill(&cwd, "repo", "repo-skill", "from repo");
|
||||
let plugin_skill_path = write_plugin_skill(
|
||||
&codex_home,
|
||||
"test",
|
||||
"sample",
|
||||
"plugin",
|
||||
"plugin-skill",
|
||||
"from plugin",
|
||||
);
|
||||
let config_layer_stack = config_stack_with_requirements(
|
||||
&codex_home,
|
||||
"",
|
||||
ConfigRequirements {
|
||||
skills: Some(Sourced::new(
|
||||
SkillsRequirementsToml {
|
||||
allowed_sources: Some(vec![
|
||||
SkillSourceRequirement::System,
|
||||
SkillSourceRequirement::Admin,
|
||||
SkillSourceRequirement::Plugin,
|
||||
]),
|
||||
},
|
||||
RequirementSource::Unknown,
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let skills_manager = SkillsManager::new(
|
||||
codex_home.path().abs(),
|
||||
/*bundled_skills_enabled*/ true,
|
||||
);
|
||||
|
||||
let outcome = skills_for_config_with_stack(
|
||||
&skills_manager,
|
||||
&cwd,
|
||||
&config_layer_stack,
|
||||
&[plugin_skill_path
|
||||
.parent()
|
||||
.and_then(std::path::Path::parent)
|
||||
.expect("plugin skills root")
|
||||
.to_path_buf()
|
||||
.abs()],
|
||||
)
|
||||
.await;
|
||||
let skill_names = outcome
|
||||
.skills
|
||||
.iter()
|
||||
.map(|skill| skill.name.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
assert!(skill_names.contains("sample:plugin-skill"));
|
||||
assert!(!skill_names.contains("user-skill"));
|
||||
assert!(!skill_names.contains("repo-skill"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn skills_for_cwd_reuses_cached_entry_even_when_entry_has_extra_roots() {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
@@ -322,6 +401,71 @@ async fn skills_for_cwd_reuses_cached_entry_even_when_entry_has_extra_roots() {
|
||||
assert_eq!(outcome_without_extra.errors, outcome_with_extra.errors);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn skills_for_cwd_separates_cache_entries_by_managed_allowed_sources() {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
let cwd = tempfile::tempdir().expect("tempdir");
|
||||
write_user_skill(&codex_home, "user", "user-skill", "from user");
|
||||
let unrestricted_stack = config_stack(&codex_home, "");
|
||||
let restricted_stack = config_stack_with_requirements(
|
||||
&codex_home,
|
||||
"",
|
||||
ConfigRequirements {
|
||||
skills: Some(Sourced::new(
|
||||
SkillsRequirementsToml {
|
||||
allowed_sources: Some(vec![SkillSourceRequirement::System]),
|
||||
},
|
||||
RequirementSource::Unknown,
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let skills_manager = SkillsManager::new(
|
||||
codex_home.path().abs(),
|
||||
/*bundled_skills_enabled*/ true,
|
||||
);
|
||||
|
||||
let unrestricted_input = SkillsLoadInput::new(
|
||||
cwd.path().abs(),
|
||||
Vec::new(),
|
||||
unrestricted_stack.clone(),
|
||||
bundled_skills_enabled_from_stack(&unrestricted_stack),
|
||||
);
|
||||
let unrestricted = skills_manager
|
||||
.skills_for_cwd(
|
||||
&unrestricted_input,
|
||||
/*force_reload*/ false,
|
||||
Some(Arc::clone(&LOCAL_FS)),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
unrestricted
|
||||
.skills
|
||||
.iter()
|
||||
.any(|skill| skill.name == "user-skill")
|
||||
);
|
||||
|
||||
let restricted_input = SkillsLoadInput::new(
|
||||
cwd.path().abs(),
|
||||
Vec::new(),
|
||||
restricted_stack.clone(),
|
||||
bundled_skills_enabled_from_stack(&restricted_stack),
|
||||
);
|
||||
let restricted = skills_manager
|
||||
.skills_for_cwd(
|
||||
&restricted_input,
|
||||
/*force_reload*/ false,
|
||||
Some(Arc::clone(&LOCAL_FS)),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
!restricted
|
||||
.skills
|
||||
.iter()
|
||||
.any(|skill| skill.name == "user-skill")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn skills_for_cwd_loads_repo_user_and_extra_roots_with_local_fs() {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
@@ -2117,6 +2117,8 @@ impl Config {
|
||||
network: network_requirements,
|
||||
filesystem: filesystem_requirements,
|
||||
guardian_policy_config_source: _,
|
||||
skills: _,
|
||||
plugin_marketplaces: _,
|
||||
} = config_layer_stack.requirements().clone();
|
||||
|
||||
let user_instructions = AgentsMdManager::load_global_instructions(Some(&codex_home))
|
||||
|
||||
Reference in New Issue
Block a user