From 1d279e7a1e77fd183bb0c99d2430cfd9dc0a617c Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Mon, 27 Apr 2026 22:45:53 +0000 Subject: [PATCH] feat: add min release age instance settings for bun and uv (#8956) Co-authored-by: Claude Opus 4.5 --- backend/src/main.rs | 40 ++++++++++------- backend/src/monitor.rs | 44 ++++++++++++++----- .../windmill-common/src/global_settings.rs | 2 + .../windmill-common/src/instance_config.rs | 4 ++ backend/windmill-worker/src/bun_executor.rs | 12 +++-- .../windmill-worker/src/python_executor.rs | 20 ++++++++- backend/windmill-worker/src/worker.rs | 2 + .../src/lib/components/instanceSettings.ts | 18 ++++++++ 8 files changed, 109 insertions(+), 33 deletions(-) diff --git a/backend/src/main.rs b/backend/src/main.rs index b5d3563b5c..ce09c7f25f 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -39,14 +39,15 @@ use windmill_common::{ agent_workers::AgentConfig, global_settings::{ AI_CONFIG_SETTING, APP_WORKSPACED_ROUTE_SETTING, AUDIT_LOG_RETENTION_DAYS_SETTING, - BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ALERTS_ON_DB_OVERSIZE_SETTING, - CRITICAL_ALERTS_ON_TOKEN_EXPIRY_SETTING, CRITICAL_ALERT_MUTE_UI_SETTING, - CRITICAL_ERROR_CHANNELS_SETTING, CUSTOM_TAGS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING, - DEFAULT_TAGS_WORKSPACES_SETTING, DISABLE_PASSWORD_LOGIN_SETTING, EMAIL_DOMAIN_SETTING, - ENV_SETTINGS, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, - EXTRA_PIP_INDEX_URL_SETTING, FORK_WORKSPACE_TAG_APPEND_FORK_SUFFIX_SETTING, - HTTP_ROUTE_WORKSPACED_ROUTE_SETTING, HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING, - INDEXER_SETTING, INSTANCE_EVENTS_WEBHOOK_SETTING, INSTANCE_PYTHON_VERSION_SETTING, + BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, BUN_INSTALL_MIN_RELEASE_AGE_SETTING, + CRITICAL_ALERTS_ON_DB_OVERSIZE_SETTING, CRITICAL_ALERTS_ON_TOKEN_EXPIRY_SETTING, + CRITICAL_ALERT_MUTE_UI_SETTING, CRITICAL_ERROR_CHANNELS_SETTING, CUSTOM_TAGS_SETTING, + DEFAULT_TAGS_PER_WORKSPACE_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING, + DISABLE_PASSWORD_LOGIN_SETTING, EMAIL_DOMAIN_SETTING, ENV_SETTINGS, + EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING, + FORK_WORKSPACE_TAG_APPEND_FORK_SUFFIX_SETTING, HTTP_ROUTE_WORKSPACED_ROUTE_SETTING, + HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING, INDEXER_SETTING, + INSTANCE_EVENTS_WEBHOOK_SETTING, INSTANCE_PYTHON_VERSION_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, JOB_ISOLATION_SETTING, JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MAVEN_REPOS_SETTING, MAVEN_SETTINGS_XML_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NO_DEFAULT_MAVEN_SETTING, @@ -56,7 +57,7 @@ use windmill_common::{ REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RESTART_COORDINATION_SETTING, RETENTION_PERIOD_SECS_SETTING, RUBY_REPOS_SETTING, SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, SMTP_SETTING, TEAMS_SETTING, TIMEOUT_WAIT_RESULT_SETTING, - UV_INDEX_STRATEGY_SETTING, WORKSPACE_REGISTRIES_SETTING, + UV_EXCLUDE_NEWER_SETTING, UV_INDEX_STRATEGY_SETTING, WORKSPACE_REGISTRIES_SETTING, }, scripts::ScriptLang, stats_oss::schedule_stats, @@ -105,15 +106,16 @@ use crate::monitor::{ load_require_preexisting_user, load_tag_per_workspace_enabled, load_tag_per_workspace_workspaces, monitor_db, reload_app_workspaced_route_setting, reload_audit_log_retention_days_setting, reload_base_url_setting, - reload_bunfig_install_scopes_setting, reload_critical_alert_mute_ui_setting, - reload_critical_alerts_on_token_expiry_setting, reload_critical_error_channels_setting, - reload_extra_pip_index_url_setting, reload_http_route_workspaced_route_setting, - reload_hub_api_secret_setting, reload_hub_base_url_setting, - reload_instance_events_webhook_setting, reload_job_default_timeout_setting, - reload_job_isolation_setting, reload_jwt_secret_setting, reload_license_key, - reload_npm_config_registry_setting, reload_otel_tracing_proxy_setting, + reload_bun_install_min_release_age_setting, reload_bunfig_install_scopes_setting, + reload_critical_alert_mute_ui_setting, reload_critical_alerts_on_token_expiry_setting, + reload_critical_error_channels_setting, reload_extra_pip_index_url_setting, + reload_http_route_workspaced_route_setting, reload_hub_api_secret_setting, + reload_hub_base_url_setting, reload_instance_events_webhook_setting, + reload_job_default_timeout_setting, reload_job_isolation_setting, reload_jwt_secret_setting, + reload_license_key, reload_npm_config_registry_setting, reload_otel_tracing_proxy_setting, reload_pip_index_url_setting, reload_retention_period_setting, reload_scim_token_setting, - reload_smtp_config, reload_uv_index_strategy_setting, reload_worker_config, MonitorIteration, + reload_smtp_config, reload_uv_exclude_newer_setting, reload_uv_index_strategy_setting, + reload_worker_config, MonitorIteration, }; #[cfg(feature = "parquet")] @@ -1742,6 +1744,10 @@ async fn process_notify_event( EXTRA_PIP_INDEX_URL_SETTING => reload_extra_pip_index_url_setting(conn).await, PIP_INDEX_URL_SETTING => reload_pip_index_url_setting(conn).await, UV_INDEX_STRATEGY_SETTING => reload_uv_index_strategy_setting(conn).await, + UV_EXCLUDE_NEWER_SETTING => reload_uv_exclude_newer_setting(conn).await, + BUN_INSTALL_MIN_RELEASE_AGE_SETTING => { + reload_bun_install_min_release_age_setting(conn).await + } INSTANCE_PYTHON_VERSION_SETTING => { reload_instance_python_version_setting(conn).await } diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index c360bb7b8d..ef3fb99696 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -53,11 +53,11 @@ use windmill_common::{ flow_status::{FlowStatus, FlowStatusModule}, global_settings::{ AUDIT_LOG_RETENTION_DAYS_SETTING, BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, - CRITICAL_ALERTS_ON_DB_OVERSIZE_SETTING, CRITICAL_ALERTS_ON_TOKEN_EXPIRY_SETTING, - CRITICAL_ALERT_MUTE_UI_SETTING, CRITICAL_ERROR_CHANNELS_SETTING, - DEFAULT_TAGS_PER_WORKSPACE_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING, - DISABLE_PASSWORD_LOGIN, DISABLE_PASSWORD_LOGIN_SETTING, EXPOSE_DEBUG_METRICS_SETTING, - EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING, + BUN_INSTALL_MIN_RELEASE_AGE_SETTING, CRITICAL_ALERTS_ON_DB_OVERSIZE_SETTING, + CRITICAL_ALERTS_ON_TOKEN_EXPIRY_SETTING, CRITICAL_ALERT_MUTE_UI_SETTING, + CRITICAL_ERROR_CHANNELS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING, + DEFAULT_TAGS_WORKSPACES_SETTING, DISABLE_PASSWORD_LOGIN, DISABLE_PASSWORD_LOGIN_SETTING, + EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING, FORK_WORKSPACE_TAG_APPEND_FORK_SUFFIX_SETTING, HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING, INSTANCE_PYTHON_VERSION_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, JOB_ISOLATION_SETTING, JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, @@ -66,7 +66,7 @@ use windmill_common::{ POWERSHELL_REPO_PAT_SETTING, POWERSHELL_REPO_URL_SETTING, PREVIEW_TAGS_OVERRIDE_SETTING, REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING, SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, - TIMEOUT_WAIT_RESULT_SETTING, UV_INDEX_STRATEGY_SETTING, + TIMEOUT_WAIT_RESULT_SETTING, UV_EXCLUDE_NEWER_SETTING, UV_INDEX_STRATEGY_SETTING, }, indexer::load_indexer_config, jwt::JWT_SECRET, @@ -103,11 +103,11 @@ use windmill_queue::{cancel_job, get_queued_job_v2, SameWorkerPayload}; use windmill_worker::{ result_processor::handle_job_error, JobCompletedSender, JobIsolationLevel, OtelTracingProxySettings, SameWorkerSender, WorkspaceRegistryMap, BUNFIG_INSTALL_SCOPES, - CARGO_REGISTRIES, INSTANCE_PYTHON_VERSION, JAVA_HOME_DIR, JOB_DEFAULT_TIMEOUT, JOB_ISOLATION, - KEEP_JOB_DIR, MAVEN_REPOS, MAVEN_SETTINGS_XML, NO_DEFAULT_MAVEN, NPMRC, NPM_CONFIG_REGISTRY, - NSJAIL_AVAILABLE, NUGET_CONFIG, OTEL_TRACING_PROXY_SETTINGS, PIP_EXTRA_INDEX_URL, - PIP_INDEX_URL, POWERSHELL_REPO_PAT, POWERSHELL_REPO_URL, UNSHARE_PATH, UV_INDEX_STRATEGY, - WORKSPACE_REGISTRIES, + BUN_INSTALL_MIN_RELEASE_AGE, CARGO_REGISTRIES, INSTANCE_PYTHON_VERSION, JAVA_HOME_DIR, + JOB_DEFAULT_TIMEOUT, JOB_ISOLATION, KEEP_JOB_DIR, MAVEN_REPOS, MAVEN_SETTINGS_XML, + NO_DEFAULT_MAVEN, NPMRC, NPM_CONFIG_REGISTRY, NSJAIL_AVAILABLE, NUGET_CONFIG, + OTEL_TRACING_PROXY_SETTINGS, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, POWERSHELL_REPO_PAT, + POWERSHELL_REPO_URL, UNSHARE_PATH, UV_EXCLUDE_NEWER, UV_INDEX_STRATEGY, WORKSPACE_REGISTRIES, }; #[cfg(feature = "parquet")] @@ -368,6 +368,8 @@ pub async fn initial_load( reload_extra_pip_index_url_setting(&conn).await; reload_pip_index_url_setting(&conn).await; reload_uv_index_strategy_setting(&conn).await; + reload_uv_exclude_newer_setting(&conn).await; + reload_bun_install_min_release_age_setting(&conn).await; reload_npm_config_registry_setting(&conn).await; reload_bunfig_install_scopes_setting(&conn).await; reload_npmrc_setting(&conn).await; @@ -1592,6 +1594,26 @@ pub async fn reload_uv_index_strategy_setting(conn: &Connection) { .await; } +pub async fn reload_uv_exclude_newer_setting(conn: &Connection) { + reload_option_setting_with_tracing( + conn, + UV_EXCLUDE_NEWER_SETTING, + "UV_EXCLUDE_NEWER", + UV_EXCLUDE_NEWER.clone(), + ) + .await; +} + +pub async fn reload_bun_install_min_release_age_setting(conn: &Connection) { + reload_option_setting_with_tracing( + conn, + BUN_INSTALL_MIN_RELEASE_AGE_SETTING, + "BUN_INSTALL_MIN_RELEASE_AGE", + BUN_INSTALL_MIN_RELEASE_AGE.clone(), + ) + .await; +} + pub async fn reload_instance_python_version_setting(conn: &Connection) { reload_option_setting_with_tracing( conn, diff --git a/backend/windmill-common/src/global_settings.rs b/backend/windmill-common/src/global_settings.rs index 6e6486666c..c97cc44dca 100644 --- a/backend/windmill-common/src/global_settings.rs +++ b/backend/windmill-common/src/global_settings.rs @@ -29,6 +29,8 @@ pub const CARGO_REGISTRIES_SETTING: &str = "cargo_registries"; pub const EXTRA_PIP_INDEX_URL_SETTING: &str = "pip_extra_index_url"; pub const PIP_INDEX_URL_SETTING: &str = "pip_index_url"; pub const UV_INDEX_STRATEGY_SETTING: &str = "uv_index_strategy"; +pub const UV_EXCLUDE_NEWER_SETTING: &str = "uv_exclude_newer"; +pub const BUN_INSTALL_MIN_RELEASE_AGE_SETTING: &str = "bun_install_min_release_age"; pub const INSTANCE_PYTHON_VERSION_SETTING: &str = "instance_python_version"; pub const RUFF_CONFIG_SETTING: &str = "ruff_config"; pub const SCIM_TOKEN_SETTING: &str = "scim_token"; diff --git a/backend/windmill-common/src/instance_config.rs b/backend/windmill-common/src/instance_config.rs index e4391aedee..e9a9731f74 100644 --- a/backend/windmill-common/src/instance_config.rs +++ b/backend/windmill-common/src/instance_config.rs @@ -213,6 +213,10 @@ pub struct GlobalSettings { #[serde(skip_serializing_if = "Option::is_none")] pub job_default_timeout: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub bun_install_min_release_age: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub uv_exclude_newer: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub request_size_limit_mb: Option, #[serde(skip_serializing_if = "Option::is_none")] pub timeout_wait_result: Option, diff --git a/backend/windmill-worker/src/bun_executor.rs b/backend/windmill-worker/src/bun_executor.rs index a519b182b9..090b929648 100644 --- a/backend/windmill-worker/src/bun_executor.rs +++ b/backend/windmill-worker/src/bun_executor.rs @@ -22,9 +22,9 @@ use crate::{ get_proxy_envs_for_lang, handle_child::handle_child, is_sandboxing_enabled, read_ee_registry_with_workspace_override, BUNFIG_INSTALL_SCOPES, - BUN_BUNDLE_CACHE_DIR, BUN_CACHE_DIR, BUN_NO_CACHE, BUN_PATH, DISABLE_NUSER, HOME_ENV, - NODE_BIN_PATH, NODE_PATH, NPMRC, NPM_CONFIG_REGISTRY, NPM_PATH, NSJAIL_AVAILABLE, NSJAIL_PATH, - PATH_ENV, PROXY_ENVS, TRACING_PROXY_CA_CERT_PATH, TZ_ENV, + BUN_BUNDLE_CACHE_DIR, BUN_CACHE_DIR, BUN_INSTALL_MIN_RELEASE_AGE, BUN_NO_CACHE, BUN_PATH, + DISABLE_NUSER, HOME_ENV, NODE_BIN_PATH, NODE_PATH, NPMRC, NPM_CONFIG_REGISTRY, NPM_PATH, + NSJAIL_AVAILABLE, NSJAIL_PATH, PATH_ENV, PROXY_ENVS, TRACING_PROXY_CA_CERT_PATH, TZ_ENV, }; use windmill_common::{ client::AuthedClient, @@ -3240,6 +3240,12 @@ pub async fn get_common_bun_proc_envs(base_internal_url: Option<&str>) -> HashMa if let Some(ref node_path) = NODE_PATH.as_ref() { bun_envs.insert(String::from("NODE_PATH"), node_path.to_string()); } + if let Some(secs) = *BUN_INSTALL_MIN_RELEASE_AGE.read().await { + bun_envs.insert( + String::from("BUN_INSTALL_MINIMUM_RELEASE_AGE"), + secs.to_string(), + ); + } #[cfg(windows)] { diff --git a/backend/windmill-worker/src/python_executor.rs b/backend/windmill-worker/src/python_executor.rs index 30a6515c0f..5fb1278976 100644 --- a/backend/windmill-worker/src/python_executor.rs +++ b/backend/windmill-worker/src/python_executor.rs @@ -142,7 +142,7 @@ use crate::{ worker_utils::ping_job_status, PyV, DISABLE_NUSER, HOME_ENV, NSJAIL_AVAILABLE, NSJAIL_PATH, PATH_ENV, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, PROXY_ENVS, PY_INSTALL_DIR, TRACING_PROXY_CA_CERT_PATH, TZ_ENV, UV_CACHE_DIR, - UV_INDEX_STRATEGY, + UV_EXCLUDE_NEWER, UV_INDEX_STRATEGY, }; use windmill_common::client::AuthedClient; @@ -231,6 +231,7 @@ pub async fn uv_pip_compile( let uv_index_strategy = UV_INDEX_STRATEGY.read().await.clone(); let uv_index_strategy = uv_index_strategy.as_deref().unwrap_or("unsafe-best-match"); + let uv_exclude_newer = (*UV_EXCLUDE_NEWER.read().await).map(|secs| format!("{}s", secs)); let py_version_str = py_version.clone().to_string(); // Include python version to requirements.in @@ -242,8 +243,12 @@ pub async fn uv_pip_compile( let requirements = replace_pip_secret(conn, w_id, &requirements, worker_name, job_id).await?; let ws_suffix = crate::workspace_registry_cache_suffix(w_id).await; + let exclude_newer_suffix = uv_exclude_newer + .as_deref() + .map(|v| format!("-en{}", v)) + .unwrap_or_default(); let req_hash = format!( - "py-{}-{uv_index_strategy}{ws_suffix}", + "py-{}-{uv_index_strategy}{exclude_newer_suffix}{ws_suffix}", calculate_hash(&requirements) ); @@ -333,6 +338,9 @@ pub async fn uv_pip_compile( if *NATIVE_CERT { args.extend(["--native-tls"]); } + if let Some(exclude_newer) = uv_exclude_newer.as_deref() { + args.extend(["--exclude-newer", exclude_newer]); + } tracing::debug!("uv args: {:?}", args); #[cfg(windows)] @@ -1901,6 +1909,8 @@ async fn spawn_uv_install( let uv_index_strategy = uv_index_strategy_guard .as_deref() .unwrap_or("unsafe-best-match"); + let uv_exclude_newer = (*UV_EXCLUDE_NEWER.read().await).map(|secs| format!("{}s", secs)); + let uv_exclude_newer = uv_exclude_newer.as_deref(); if is_sandboxing_enabled() { tracing::info!( @@ -1936,6 +1946,9 @@ async fn spawn_uv_install( vars.push(("REQ", &req)); vars.push(("TARGET", venv_p)); vars.push(("UV_INDEX_STRATEGY", uv_index_strategy)); + if let Some(v) = uv_exclude_newer { + vars.push(("UV_EXCLUDE_NEWER", v)); + } std::fs::create_dir_all(venv_p)?; let nsjail_proto = format!("{req}.config.proto"); @@ -2007,6 +2020,9 @@ async fn spawn_uv_install( command_args.extend(["--extra-index-url", url]); }); } + if let Some(v) = uv_exclude_newer { + command_args.extend(["--exclude-newer", v]); + } let mut envs = vec![("PATH", PATH_ENV.as_str())]; envs.push(("HOME", HOME_ENV.as_str())); diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index 264dd31eac..fa22c1c76e 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -634,6 +634,8 @@ lazy_static::lazy_static! { pub static ref PIP_EXTRA_INDEX_URL: Arc>> = Arc::new(RwLock::new(None)); pub static ref PIP_INDEX_URL: Arc>> = Arc::new(RwLock::new(None)); pub static ref UV_INDEX_STRATEGY: Arc>> = Arc::new(RwLock::new(None)); + pub static ref UV_EXCLUDE_NEWER: Arc>> = Arc::new(RwLock::new(None)); + pub static ref BUN_INSTALL_MIN_RELEASE_AGE: Arc>> = Arc::new(RwLock::new(None)); pub static ref INSTANCE_PYTHON_VERSION: Arc>> = Arc::new(RwLock::new(None)); pub static ref JOB_DEFAULT_TIMEOUT: Arc>> = Arc::new(RwLock::new(None)); diff --git a/frontend/src/lib/components/instanceSettings.ts b/frontend/src/lib/components/instanceSettings.ts index c2869beafd..36fd23ff8e 100644 --- a/frontend/src/lib/components/instanceSettings.ts +++ b/frontend/src/lib/components/instanceSettings.ts @@ -509,6 +509,24 @@ export const settings: Record = { ee_only: '', hiddenIfEmpty: true }, + { + label: 'Minimum release age (uv / Python)', + description: + 'Refuse to install Python packages younger than this many seconds. Protects against supply-chain attacks via freshly published versions. Wires to uv pip --exclude-newer.', + key: 'uv_exclude_newer', + fieldType: 'seconds', + placeholder: '604800', + storage: 'setting' + }, + { + label: 'Minimum release age (bun / npm)', + description: + 'Refuse to install npm packages younger than this many seconds. Protects against supply-chain attacks via freshly published versions. Sets BUN_INSTALL_MINIMUM_RELEASE_AGE.', + key: 'bun_install_min_release_age', + fieldType: 'seconds', + placeholder: '604800', + storage: 'setting' + }, { label: 'Nuget Config', description: