Respect config layers when removing plugin marketplaces (#40683)

## Why

Removing a base-user marketplace must not delete a snapshot that is still
referenced by another enabled configuration layer.

## What changed

- Load the current configuration stack for marketplace commands in the CLI and
  app server.
- Reject removal when a matching marketplace is defined by another enabled
  layer, and identify the configuration source that must be updated instead.
- Allow cleanup of an installed marketplace snapshot even when no base-user
  entry exists.

## Testing

Cover project, enterprise-managed, system, and session-flag configurations, as
well as snapshot-only removal through the CLI and app server.

GitOrigin-RevId: 4f0853a40c2b5fae0410696b79eee4284239c7a9
This commit is contained in:
willwang-openai
2026-08-25 19:38:39 +00:00
committed by copyberry
parent f1a6f7c9b3
commit a6e63f9f32
7 changed files with 253 additions and 47 deletions
+6
View File
@@ -295,6 +295,12 @@ Example with notification opt-out:
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads, including multiple `desktop.*` edits. Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and personality defaults do not reload existing threads.
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including exact managed values (`cliAuthCredentialsStore`, `chatgptBaseUrl`, `sqliteHome`, `logDir`, `modelCatalogJson`, `checkForUpdateOnStartup`, `allowLoginShell`, `feedback.enabled`, and `windowsSandboxPrivateDesktop`), requirements-only developer instructions (`additionalDeveloperInstructions`, supplied independently of ordinary developer instructions), allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), the Browser/Computer Use umbrella policy (`allowBrowserAndComputerUse`), computer use policy (`computerUse`, including persistent approval, default application access, and per-platform application rules), Browser Use policy (`browserUse`, including history access, origin rules, auto-review, and approval controls), interactive browser import policy (`inAppBrowser.allowExternalBrowserSettingsImport`), pinned feature values (`featureRequirements`, including the default-allowed `in_app_updates` policy that administrators can set to `false`), managed lifecycle hooks (`hooks`, including command handlers with optional `additionalContextLimit` and `mcp_tool` handlers with `server`, `tool`, `input`, `timeoutSec`, and `statusMessage`), `enforceResidency`, managed automatic review (`autoReview.requiredOnModels` and `autoReview.ignoreRules`), model defaults (`models.newThread.model`, `models.newThread.modelReasoningEffort`, and `models.newThread.serviceTier`), and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`.
### Plugin configuration scope
`marketplace/remove` rejects removal when the marketplace name is defined in another
enabled layer of the operation's loaded config stack. Otherwise it removes the
snapshot and any base-user entry; a base-user entry is not required for cleanup.
### Example: Start or resume a thread
Start a fresh thread when you need a new Codex conversation.
@@ -51,8 +51,10 @@ impl MarketplaceRequestProcessor {
&self,
params: MarketplaceRemoveParams,
) -> Result<MarketplaceRemoveResponse, JSONRPCErrorError> {
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
remove_marketplace(
self.config.codex_home.to_path_buf(),
config.codex_home.to_path_buf(),
config.config_layer_stack,
CoreMarketplaceRemoveRequest {
marketplace_name: params.marketplace_name,
},
@@ -9,9 +9,12 @@ use codex_app_server_protocol::MarketplaceRemoveResponse;
use codex_app_server_protocol::RequestId;
use codex_config::MarketplaceConfigUpdate;
use codex_config::record_user_marketplace;
use codex_core::config::set_project_trust_level;
use codex_core_plugins::installed_marketplaces::marketplace_install_root;
use codex_protocol::config_types::TrustLevel;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use test_case::test_case;
use tokio::time::timeout;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
@@ -43,16 +46,19 @@ fn canonicalize_path_with_existing_parent(path: &std::path::Path) -> Result<std:
Ok(parent.canonicalize()?.join(file_name))
}
#[test_case(false; "snapshot only")]
#[test_case(true; "user entry and snapshot")]
#[tokio::test]
async fn marketplace_remove_deletes_config_and_installed_root() -> Result<()> {
async fn marketplace_remove_deletes_config_and_installed_root(user_entry: bool) -> Result<()> {
let codex_home = TempDir::new()?;
record_user_marketplace(codex_home.path(), "debug", &configured_marketplace_update())?;
if user_entry {
record_user_marketplace(codex_home.path(), "debug", &configured_marketplace_update())?;
}
write_installed_marketplace(codex_home.path(), "debug")?;
let installed_root = marketplace_install_root(codex_home.path()).join("debug");
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
.without_auto_env()
.build_initialized()
.await?;
let response: MarketplaceRemoveResponse = mcp
@@ -72,8 +78,13 @@ async fn marketplace_remove_deletes_config_and_installed_root() -> Result<()> {
canonicalize_path_with_existing_parent(&installed_root)?,
);
let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?;
assert!(!config.contains("[marketplaces.debug]"));
let config_path = codex_home.path().join("config.toml");
if user_entry {
let config = std::fs::read_to_string(config_path)?;
assert!(!config.contains("[marketplaces.debug]"));
} else {
assert!(!config_path.exists());
}
assert!(
!marketplace_install_root(codex_home.path())
.join("debug")
@@ -111,3 +122,54 @@ async fn marketplace_remove_rejects_unknown_marketplace() -> Result<()> {
);
Ok(())
}
#[test_case(false; "project only")]
#[test_case(true; "project and user")]
#[tokio::test]
async fn marketplace_remove_preserves_project_marketplace(user_entry: bool) -> Result<()> {
let codex_home = TempDir::new()?;
if user_entry {
record_user_marketplace(codex_home.path(), "debug", &configured_marketplace_update())?;
}
// TestAppServer starts in CODEX_HOME, so make it a trusted project as well.
std::fs::create_dir_all(codex_home.path().join(".git"))?;
std::fs::create_dir_all(codex_home.path().join(".codex"))?;
let project_config_path = codex_home.path().join(".codex/config.toml");
let project_config = "[marketplaces.debug]\nsource_type = \"git\"\nsource = \"https://github.com/owner/repo.git\"\n";
std::fs::write(&project_config_path, project_config)?;
set_project_trust_level(codex_home.path(), codex_home.path(), TrustLevel::Trusted)?;
write_installed_marketplace(codex_home.path(), "debug")?;
let snapshot_path =
marketplace_install_root(codex_home.path()).join("debug/.agents/plugins/marketplace.json");
let user_config_path = codex_home.path().join("config.toml");
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized()
.await?;
let user_config = std::fs::read_to_string(&user_config_path)?;
let request_id = mcp
.send_marketplace_remove_request(MarketplaceRemoveParams {
marketplace_name: "debug".to_string(),
})
.await?;
let err = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(err.error.code, -32600);
assert!(
err.error
.message
.starts_with("marketplace `debug` is configured in project (")
);
assert_eq!(std::fs::read_to_string(user_config_path)?, user_config);
assert_eq!(
std::fs::read_to_string(project_config_path)?,
project_config
);
assert_eq!(std::fs::read_to_string(snapshot_path)?, "{}");
Ok(())
}
+14 -26
View File
@@ -3,7 +3,7 @@ use anyhow::Result;
use anyhow::bail;
use clap::Parser;
use codex_core::config::Config;
use codex_core::config::find_codex_home;
use codex_core::config::LoaderOverrides;
use codex_core::plugins_manager_for_config;
use codex_core_plugins::PluginMarketplaceUpgradeOutcome;
use codex_core_plugins::PluginsConfigInput;
@@ -23,6 +23,7 @@ use std::collections::HashSet;
use std::path::Path;
use std::path::PathBuf;
use crate::cloud_config;
use crate::plugin_cmd::JsonMarketplaceSource;
use crate::plugin_cmd::configured_marketplace_snapshot_issues;
use crate::plugin_cmd::configured_marketplace_sources;
@@ -127,22 +128,21 @@ impl MarketplaceCli {
subcommand,
} = self;
let overrides = config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
let config =
cloud_config::load_config(&config_overrides, LoaderOverrides::default()).await?;
match subcommand {
MarketplaceSubcommand::Add(args) => run_add(overrides, args).await?,
MarketplaceSubcommand::List(args) => run_list(overrides, args).await?,
MarketplaceSubcommand::Upgrade(args) => run_upgrade(overrides, args).await?,
MarketplaceSubcommand::Remove(args) => run_remove(args).await?,
MarketplaceSubcommand::Add(args) => run_add(config, args).await?,
MarketplaceSubcommand::List(args) => run_list(config, args).await?,
MarketplaceSubcommand::Upgrade(args) => run_upgrade(config, args).await?,
MarketplaceSubcommand::Remove(args) => run_remove(config, args).await?,
}
Ok(())
}
}
async fn run_add(overrides: Vec<(String, toml::Value)>, args: AddMarketplaceArgs) -> Result<()> {
async fn run_add(config: Config, args: AddMarketplaceArgs) -> Result<()> {
let AddMarketplaceArgs {
source,
ref_name,
@@ -150,9 +150,6 @@ async fn run_add(overrides: Vec<(String, toml::Value)>, args: AddMarketplaceArgs
json,
} = args;
let config = Config::load_with_cli_overrides(overrides)
.await
.context("failed to load configuration")?;
let outcome = add_marketplace(
config.codex_home.to_path_buf(),
config.config_layer_stack.requirements().clone(),
@@ -207,10 +204,7 @@ impl JsonMarketplaceAddOutput {
}
}
async fn run_list(overrides: Vec<(String, toml::Value)>, args: ListMarketplaceArgs) -> Result<()> {
let config = Config::load_with_cli_overrides(overrides)
.await
.context("failed to load configuration")?;
async fn run_list(config: Config, args: ListMarketplaceArgs) -> Result<()> {
let manager = plugins_manager_for_config(&config, load_cli_auth_manager(&config).await?);
let plugins_input = config.plugins_config_input();
let marketplace_listing = manager
@@ -366,17 +360,11 @@ fn configured_marketplace_sources_by_root(
.collect()
}
async fn run_upgrade(
overrides: Vec<(String, toml::Value)>,
args: UpgradeMarketplaceArgs,
) -> Result<()> {
async fn run_upgrade(config: Config, args: UpgradeMarketplaceArgs) -> Result<()> {
let UpgradeMarketplaceArgs {
marketplace_name,
json,
} = args;
let config = Config::load_with_cli_overrides(overrides)
.await
.context("failed to load configuration")?;
let manager = plugins_manager_for_config(&config, load_cli_auth_manager(&config).await?);
let plugins_input = config.plugins_config_input();
let outcome = manager
@@ -389,14 +377,14 @@ async fn run_upgrade(
}
}
async fn run_remove(args: RemoveMarketplaceArgs) -> Result<()> {
async fn run_remove(config: Config, args: RemoveMarketplaceArgs) -> Result<()> {
let RemoveMarketplaceArgs {
marketplace_name,
json,
} = args;
let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;
let outcome = remove_marketplace(
codex_home.to_path_buf(),
config.codex_home.to_path_buf(),
config.config_layer_stack,
MarketplaceRemoveRequest { marketplace_name },
)
.await?;
@@ -11,6 +11,7 @@ use codex_config::ConfigLoadOptions;
use codex_config::types::AuthCredentialsStoreMode;
use codex_core::config::load_config_toml_with_layer_stack;
use codex_core::config::load_global_mcp_servers;
use codex_core_plugins::installed_marketplaces::marketplace_install_root;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use serde_json::Value;
@@ -37,14 +38,14 @@ const MANAGED_SCOPE: &str = "managed.read";
const MOCK_ACCESS_TOKEN: &str = "mock-managed-access-token";
const MOCK_REFRESH_TOKEN: &str = "mock-managed-refresh-token";
struct CloudManagedMcpFixture {
struct CloudManagedConfigFixture {
server: MockServer,
codex_home: TempDir,
user_config: String,
mcp_url: String,
}
impl CloudManagedMcpFixture {
impl CloudManagedConfigFixture {
async fn new() -> Result<Option<Self>> {
let server = MockServer::start().await;
let chatgpt_base_url = format!("{}/backend-api", server.uri());
@@ -67,7 +68,7 @@ impl CloudManagedMcpFixture {
!= Some(chatgpt_base_url.as_str())
{
eprintln!(
"skipping cloud-managed MCP subprocess: host-managed authentication or backend routing prevents isolated mock credentials"
"skipping cloud-managed subprocess: host-managed authentication or backend routing prevents isolated mock credentials"
);
return Ok(None);
}
@@ -90,7 +91,10 @@ impl CloudManagedMcpFixture {
auth = \"oauth\"\n\
scopes = [\"{MANAGED_SCOPE}\"]\n\n\
[mcp_servers.{MANAGED_SERVER_NAME}.oauth]\n\
client_id = \"{MANAGED_CLIENT_ID}\"\n"
client_id = \"{MANAGED_CLIENT_ID}\"\n\n\
[marketplaces.managed]\n\
source_type = \"git\"\n\
source = \"https://github.com/owner/repo.git\"\n"
);
Mock::given(method("GET"))
.and(path("/backend-api/wham/config/bundle"))
@@ -99,8 +103,8 @@ impl CloudManagedMcpFixture {
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"config_toml": {
"enterprise_managed": [{
"id": "managed-mcp-config",
"name": "Managed MCP servers",
"id": "managed-config",
"name": "Managed resources",
"contents": managed_config,
}],
},
@@ -156,7 +160,7 @@ impl CloudManagedMcpFixture {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn list_and_get_resolve_cloud_managed_mcp_without_writing_user_config() -> Result<()> {
let Some(fixture) = CloudManagedMcpFixture::new().await? else {
let Some(fixture) = CloudManagedConfigFixture::new().await? else {
return Ok(());
};
@@ -187,7 +191,7 @@ async fn list_and_get_resolve_cloud_managed_mcp_without_writing_user_config() ->
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn login_and_logout_persist_only_cloud_managed_mcp_oauth_credentials() -> Result<()> {
let Some(fixture) = CloudManagedMcpFixture::new().await? else {
let Some(fixture) = CloudManagedConfigFixture::new().await? else {
return Ok(());
};
@@ -382,11 +386,29 @@ async fn login_and_logout_persist_only_cloud_managed_mcp_oauth_credentials() ->
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn add_and_remove_do_not_copy_or_delete_cloud_managed_mcp_servers() -> Result<()> {
let Some(fixture) = CloudManagedMcpFixture::new().await? else {
async fn add_and_remove_preserve_cloud_managed_resources() -> Result<()> {
let Some(fixture) = CloudManagedConfigFixture::new().await? else {
return Ok(());
};
let installed_root = marketplace_install_root(fixture.codex_home.path()).join("managed");
std::fs::create_dir_all(&installed_root)?;
let marker = installed_root.join("marker.txt");
std::fs::write(&marker, "installed")?;
// Exercise both the initial fetch and the cached bundle without a user entry.
for _ in 0..2 {
let output = fixture
.command(&["plugin", "marketplace", "remove", "managed"])?
.output()
.await?;
assert!(!output.status.success());
assert!(String::from_utf8(output.stderr)?.contains(
"marketplace `managed` is configured in enterprise-managed (Managed resources, managed-config); remove it from that configuration source instead"
));
fixture.assert_user_config_unchanged()?;
assert_eq!(std::fs::read_to_string(&marker)?, "installed");
}
fixture
.output(&["mcp", "get", MANAGED_SERVER_NAME, "--json"])
.await?;
+30
View File
@@ -95,3 +95,33 @@ async fn marketplace_remove_rejects_unknown_marketplace() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn marketplace_remove_preserves_marketplace_referenced_by_session_config() -> Result<()> {
let codex_home = TempDir::new()?;
record_user_marketplace(codex_home.path(), "debug", &configured_marketplace_update())?;
write_installed_marketplace(codex_home.path(), "debug")?;
let config_path = codex_home.path().join("config.toml");
let config = std::fs::read_to_string(&config_path)?;
codex_command(codex_home.path())?
.args([
"plugin", "marketplace",
"-c", "marketplaces.debug.source_type=\"git\"",
"remove", "debug",
])
.assert()
.failure()
.stderr(contains(
"marketplace `debug` is configured in session-flags; remove it from that configuration source instead",
));
assert_eq!(std::fs::read_to_string(config_path)?, config);
assert_eq!(
std::fs::read_to_string(
marketplace_install_root(codex_home.path()).join("debug/marker.txt")
)?,
"installed",
);
Ok(())
}
+101 -5
View File
@@ -1,5 +1,9 @@
use crate::installed_marketplaces::marketplace_install_root;
use codex_config::CONFIG_TOML_FILE;
use codex_config::ConfigLayerSource;
use codex_config::ConfigLayerStack;
use codex_config::RemoveMarketplaceConfigOutcome;
use codex_config::format_config_layer_source;
use codex_config::remove_user_marketplace_config;
use codex_plugin::validate_plugin_segment;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -26,25 +30,54 @@ pub enum MarketplaceRemoveError {
Internal(String),
}
/// Removes a marketplace's snapshot and any base-user entry only when no other
/// enabled layer in the current operation's config stack references its name.
pub async fn remove_marketplace(
codex_home: PathBuf,
config_layer_stack: ConfigLayerStack,
request: MarketplaceRemoveRequest,
) -> Result<MarketplaceRemoveOutcome, MarketplaceRemoveError> {
tokio::task::spawn_blocking(move || remove_marketplace_sync(codex_home.as_path(), request))
.await
.map_err(|err| {
MarketplaceRemoveError::Internal(format!("failed to remove marketplace: {err}"))
})?
tokio::task::spawn_blocking(move || {
remove_marketplace_sync(codex_home.as_path(), &config_layer_stack, request)
})
.await
.map_err(|err| {
MarketplaceRemoveError::Internal(format!("failed to remove marketplace: {err}"))
})?
}
fn remove_marketplace_sync(
codex_home: &Path,
config_layer_stack: &ConfigLayerStack,
request: MarketplaceRemoveRequest,
) -> Result<MarketplaceRemoveOutcome, MarketplaceRemoveError> {
let marketplace_name = request.marketplace_name;
validate_plugin_segment(&marketplace_name, "marketplace name")
.map_err(MarketplaceRemoveError::InvalidRequest)?;
// Removing a user override must not delete a snapshot still referenced by
// another enabled layer, including a lower-precedence layer it shadows.
let user_config_file = AbsolutePathBuf::resolve_path_against_base(CONFIG_TOML_FILE, codex_home);
if let Some(layer) = config_layer_stack.layers_high_to_low().find(|layer| {
!matches!(
&layer.name,
ConfigLayerSource::User { file, profile: None } if file == &user_config_file
) && layer
.config
.get("marketplaces")
.and_then(toml::Value::as_table)
.is_some_and(|marketplaces| {
marketplaces
.keys()
.any(|name| name.eq_ignore_ascii_case(&marketplace_name))
})
}) {
let source = format_config_layer_source(&layer.name, CONFIG_TOML_FILE);
return Err(MarketplaceRemoveError::InvalidRequest(format!(
"marketplace `{marketplace_name}` is configured in {source}; remove it from that configuration source instead"
)));
}
let destination = marketplace_install_root(codex_home).join(&marketplace_name);
let config_outcome =
remove_user_marketplace_config(codex_home, &marketplace_name).map_err(|err| {
@@ -107,6 +140,7 @@ fn remove_marketplace_root(root: &Path) -> Result<Option<AbsolutePathBuf>, Marke
#[cfg(test)]
mod tests {
use super::*;
use codex_config::ConfigLayerEntry;
use codex_config::MarketplaceConfigUpdate;
use codex_config::record_user_marketplace;
use pretty_assertions::assert_eq;
@@ -136,6 +170,7 @@ mod tests {
let outcome = remove_marketplace_sync(
codex_home.path(),
&ConfigLayerStack::default(),
MarketplaceRemoveRequest {
marketplace_name: "debug".to_string(),
},
@@ -159,6 +194,7 @@ mod tests {
let err = remove_marketplace_sync(
codex_home.path(),
&ConfigLayerStack::default(),
MarketplaceRemoveRequest {
marketplace_name: "debug".to_string(),
},
@@ -171,6 +207,62 @@ mod tests {
);
}
#[test]
fn remove_marketplace_sync_preserves_shadowed_system_marketplace() {
let codex_home = TempDir::new().unwrap();
let config = "[marketplaces.debug]\nsource_type = \"git\"\nsource = \"https://github.com/owner/repo.git\"\n";
let user_file =
AbsolutePathBuf::resolve_path_against_base(CONFIG_TOML_FILE, codex_home.path());
let system_file =
AbsolutePathBuf::resolve_path_against_base("system.toml", codex_home.path());
fs::write(&user_file, config).unwrap();
fs::write(&system_file, config).unwrap();
let installed_root = marketplace_install_root(codex_home.path()).join("debug");
fs::create_dir_all(&installed_root).unwrap();
let marker = installed_root.join("marker.txt");
fs::write(&marker, "installed").unwrap();
let stack = ConfigLayerStack::new(
vec![
ConfigLayerEntry::new(
ConfigLayerSource::System {
file: system_file.clone(),
},
toml::from_str(config).unwrap(),
),
ConfigLayerEntry::new(
ConfigLayerSource::User {
file: user_file.clone(),
profile: None,
},
toml::from_str(config).unwrap(),
),
],
Default::default(),
Default::default(),
)
.unwrap();
let err = remove_marketplace_sync(
codex_home.path(),
&stack,
MarketplaceRemoveRequest {
marketplace_name: "debug".to_string(),
},
)
.unwrap_err();
assert_eq!(
err.to_string(),
format!(
"marketplace `debug` is configured in system ({}); remove it from that configuration source instead",
system_file.display()
),
);
assert_eq!(fs::read_to_string(user_file).unwrap(), config);
assert_eq!(fs::read_to_string(system_file).unwrap(), config);
assert_eq!(fs::read_to_string(marker).unwrap(), "installed");
}
#[test]
fn remove_marketplace_sync_rejects_case_mismatched_configured_name() {
let codex_home = TempDir::new().unwrap();
@@ -190,6 +282,7 @@ mod tests {
let err = remove_marketplace_sync(
codex_home.path(),
&ConfigLayerStack::default(),
MarketplaceRemoveRequest {
marketplace_name: "Debug".to_string(),
},
@@ -219,6 +312,7 @@ mod tests {
let err = remove_marketplace_sync(
codex_home.path(),
&ConfigLayerStack::default(),
MarketplaceRemoveRequest {
marketplace_name: "debug".to_string(),
},
@@ -252,6 +346,7 @@ mod tests {
let outcome = remove_marketplace_sync(
codex_home.path(),
&ConfigLayerStack::default(),
MarketplaceRemoveRequest {
marketplace_name: "debug".to_string(),
},
@@ -288,6 +383,7 @@ marketplaces = { debug = { source_type = "git", source = "https://github.com/own
let outcome = remove_marketplace_sync(
codex_home.path(),
&ConfigLayerStack::default(),
MarketplaceRemoveRequest {
marketplace_name: "debug".to_string(),
},