Compare commits

..

3 Commits

Author SHA1 Message Date
Ruben Fiszel
0cc3342e12 chore: update sqlx offline cache
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 06:45:57 +00:00
Ruben Fiszel
d081323250 chore: update ee-repo-ref
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 06:39:46 +00:00
Ruben Fiszel
ebbc259a37 feat: add volume limits info in CE volumes drawer
Show an info alert in the volumes drawer when running in Community
Edition, mentioning the 20 volumes per workspace and 50 MB per file
limits. Update ee-repo-ref for companion EE changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 06:36:24 +00:00
38 changed files with 205 additions and 883 deletions

View File

@@ -1,28 +1,5 @@
# Changelog
## [1.651.1](https://github.com/windmill-labs/windmill/compare/v1.651.0...v1.651.1) (2026-03-05)
### Bug Fixes
* prevent slow loading toast interval from leaking on promise cancellation ([#8240](https://github.com/windmill-labs/windmill/issues/8240)) ([2e582b1](https://github.com/windmill-labs/windmill/commit/2e582b1bc1c299388a3c97cfddff9d0eb92858f2))
* suppress unused variable warnings on windows builds ([#8241](https://github.com/windmill-labs/windmill/issues/8241)) ([2d58382](https://github.com/windmill-labs/windmill/commit/2d583826dc065c05684d4cd1d1510f0d1f2d9ae9))
## [1.651.0](https://github.com/windmill-labs/windmill/compare/v1.650.0...v1.651.0) (2026-03-05)
### Features
* add sandbox annotations, volume mounts, for AI sandbox starting with claude ([#8058](https://github.com/windmill-labs/windmill/issues/8058)) ([5f0ef93](https://github.com/windmill-labs/windmill/commit/5f0ef936d1d5d07d01c8e07e26ec254feebef8fb))
* hash-based MCP tool names for long paths ([#8133](https://github.com/windmill-labs/windmill/issues/8133)) ([ce041e8](https://github.com/windmill-labs/windmill/commit/ce041e8a5e7ff105df389875d9981f3843d4ce39))
### Bug Fixes
* **python-client:** add delete_s3_object ([#8216](https://github.com/windmill-labs/windmill/issues/8216)) ([90f4c64](https://github.com/windmill-labs/windmill/commit/90f4c64ee12e1d04ce846ff88d6658f667e194e0))
* update CLI bun template to match UI template ([#8238](https://github.com/windmill-labs/windmill/issues/8238)) ([a8cbe93](https://github.com/windmill-labs/windmill/commit/a8cbe9396ffc51140dce5582d57f4dc59873304e))
* write fallback package.json for codebase mode nsjail ([#8239](https://github.com/windmill-labs/windmill/issues/8239)) ([d46913b](https://github.com/windmill-labs/windmill/commit/d46913b74a0ffd41d2323e0355cc81954f09e29d))
## [1.650.0](https://github.com/windmill-labs/windmill/compare/v1.649.0...v1.650.0) (2026-03-05)

View File

@@ -15,7 +15,7 @@
]
},
"nullable": [
false
true
]
},
"hash": "2d6607b3c38fe72b5663c32de58dacbabed4c5ae28101e3ae2694f96fd055a91"

View File

@@ -15,7 +15,7 @@
]
},
"nullable": [
true
null
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"

View File

@@ -15,7 +15,7 @@
]
},
"nullable": [
false
true
]
},
"hash": "eba16eb819e2644284fb073c891706d78a6f24cb0e614d7d81ba1b643805bf06"

143
backend/Cargo.lock generated
View File

@@ -15741,7 +15741,7 @@ dependencies = [
[[package]]
name = "windmill"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"async-nats",
@@ -15773,7 +15773,6 @@ dependencies = [
"sql-builder",
"sqlx",
"strum 0.27.2",
"tar",
"tempfile",
"tikv-jemalloc-ctl",
"tikv-jemalloc-sys",
@@ -15808,7 +15807,7 @@ dependencies = [
[[package]]
name = "windmill-alerting"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15821,7 +15820,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"argon2",
@@ -15960,7 +15959,7 @@ dependencies = [
[[package]]
name = "windmill-api-agent-workers"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15983,7 +15982,7 @@ dependencies = [
[[package]]
name = "windmill-api-assets"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15996,7 +15995,7 @@ dependencies = [
[[package]]
name = "windmill-api-auth"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16022,7 +16021,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -16032,7 +16031,7 @@ dependencies = [
[[package]]
name = "windmill-api-configs"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16049,7 +16048,7 @@ dependencies = [
[[package]]
name = "windmill-api-debug"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"axum 0.7.9",
"base64 0.22.1",
@@ -16072,7 +16071,7 @@ dependencies = [
[[package]]
name = "windmill-api-embeddings"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16095,7 +16094,7 @@ dependencies = [
[[package]]
name = "windmill-api-flow-conversations"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16111,7 +16110,7 @@ dependencies = [
[[package]]
name = "windmill-api-flows"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16131,7 +16130,7 @@ dependencies = [
[[package]]
name = "windmill-api-groups"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16151,7 +16150,7 @@ dependencies = [
[[package]]
name = "windmill-api-inputs"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16165,7 +16164,7 @@ dependencies = [
[[package]]
name = "windmill-api-integration-tests"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"async-nats",
@@ -16192,7 +16191,7 @@ dependencies = [
[[package]]
name = "windmill-api-jobs"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16217,7 +16216,7 @@ dependencies = [
[[package]]
name = "windmill-api-npm-proxy"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"axum 0.7.9",
"flate2",
@@ -16235,7 +16234,7 @@ dependencies = [
[[package]]
name = "windmill-api-openapi"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16256,7 +16255,7 @@ dependencies = [
[[package]]
name = "windmill-api-schedule"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16276,7 +16275,7 @@ dependencies = [
[[package]]
name = "windmill-api-scripts"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16306,7 +16305,7 @@ dependencies = [
[[package]]
name = "windmill-api-settings"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16333,7 +16332,7 @@ dependencies = [
[[package]]
name = "windmill-api-sse"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"lazy_static",
"serde",
@@ -16345,7 +16344,7 @@ dependencies = [
[[package]]
name = "windmill-api-users"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"argon2",
"axum 0.7.9",
@@ -16368,7 +16367,7 @@ dependencies = [
[[package]]
name = "windmill-api-workers"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16382,7 +16381,7 @@ dependencies = [
[[package]]
name = "windmill-api-workspaces"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16413,7 +16412,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"chrono",
"lazy_static",
@@ -16427,7 +16426,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16446,7 +16445,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"aes-gcm",
"anyhow",
@@ -16545,7 +16544,7 @@ dependencies = [
[[package]]
name = "windmill-dep-map"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"chrono",
"itertools 0.14.0",
@@ -16564,7 +16563,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"regex",
"serde",
@@ -16579,7 +16578,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -16603,7 +16602,7 @@ dependencies = [
[[package]]
name = "windmill-jseval"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"futures",
@@ -16620,7 +16619,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -16636,7 +16635,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16657,7 +16656,7 @@ dependencies = [
[[package]]
name = "windmill-native-triggers"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16688,7 +16687,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"async-oauth2",
@@ -16712,7 +16711,7 @@ dependencies = [
[[package]]
name = "windmill-object-store"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"async-stream",
@@ -16746,7 +16745,7 @@ dependencies = [
[[package]]
name = "windmill-operator"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"futures",
@@ -16764,7 +16763,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -16773,7 +16772,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16785,7 +16784,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16797,7 +16796,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"gosyn",
@@ -16809,7 +16808,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16821,7 +16820,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16833,7 +16832,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -16844,7 +16843,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16855,7 +16854,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16868,7 +16867,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -16892,7 +16891,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16906,7 +16905,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -16923,7 +16922,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16938,7 +16937,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16957,7 +16956,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"serde",
@@ -16968,7 +16967,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17005,7 +17004,7 @@ dependencies = [
[[package]]
name = "windmill-runtime-nativets"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"const_format",
@@ -17043,7 +17042,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"getrandom 0.3.4",
"wasm-bindgen",
@@ -17054,7 +17053,7 @@ dependencies = [
[[package]]
name = "windmill-store"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17083,7 +17082,7 @@ dependencies = [
[[package]]
name = "windmill-test-utils"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -17106,7 +17105,7 @@ dependencies = [
[[package]]
name = "windmill-trigger"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17139,7 +17138,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-email"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17159,7 +17158,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-gcp"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17193,7 +17192,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-http"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17228,7 +17227,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-kafka"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17251,7 +17250,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-mqtt"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17275,7 +17274,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-nats"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"async-nats",
@@ -17299,7 +17298,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-postgres"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17334,7 +17333,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-sqs"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17362,7 +17361,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-websocket"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17385,7 +17384,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"bitflags 2.9.4",
@@ -17403,7 +17402,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"anyhow",
"async-once-cell",
@@ -17509,7 +17508,7 @@ dependencies = [
[[package]]
name = "windmill-worker-volumes"
version = "1.651.1"
version = "1.650.0"
dependencies = [
"bytes",
"futures",

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.651.1"
version = "1.650.0"
authors.workspace = true
edition.workspace = true
@@ -77,7 +77,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.651.1"
version = "1.650.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -257,7 +257,6 @@ axum.workspace = true
serde.workspace = true
windmill-api-client.workspace = true
tempfile.workspace = true
tar.workspace = true
windmill-parser-ts.workspace = true
rumqttc.workspace = true
rdkafka.workspace = true

View File

@@ -1 +1 @@
f9549c813b3dba5324ea9d1edacc8756a6d699bf
05385738e36e81f5bc51d15c0ca60bba30457c21

View File

@@ -1,6 +1,5 @@
use sqlx::postgres::Postgres;
use sqlx::Pool;
use uuid::Uuid;
use windmill_common::jobs::{JobPayload, RawCode};
use windmill_common::scripts::ScriptLang;
use windmill_test_utils::*;
@@ -1449,240 +1448,3 @@ export function main() { return { a, b }; }
);
}
}
// ============================================================================
// Codebase Mode Tests
// ============================================================================
/// Create a TAR archive in memory containing a single `main.js` file.
fn create_codebase_tar(main_js_content: &str) -> Vec<u8> {
let mut builder = tar::Builder::new(Vec::new());
let content = main_js_content.as_bytes();
let mut header = tar::Header::new_gnu();
header.set_path("main.js").unwrap();
header.set_size(content.len() as u64);
header.set_mode(0o644);
header.set_cksum();
builder.append(&header, content).unwrap();
builder.into_inner().unwrap()
}
/// Place a TAR codebase at the expected cache path for the given job ID and hash.
fn place_codebase_in_cache(job_id: &Uuid, tar_bytes: &[u8], is_esm: bool) {
let codebase_id = if is_esm {
format!("{}.esm.tar", job_id)
} else {
format!("{}.tar", job_id)
};
let bundle_path = format!("script_bundle/test-workspace/{}", codebase_id);
let cache_path = format!(
"{}/{}.tar",
*windmill_common::worker::ROOT_CACHE_NOMOUNT_DIR,
bundle_path,
);
let parent = std::path::Path::new(&cache_path).parent().unwrap();
std::fs::create_dir_all(parent).unwrap();
std::fs::write(&cache_path, tar_bytes).unwrap();
}
#[sqlx::test(fixtures("base"))]
async fn test_cjs_codebase_tar(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let main_js = r#"
module.exports.main = function() {
return "cjs codebase ok";
};
"#;
let inner_content = r#"export function main() { return "cjs codebase ok"; }"#;
let job_id = Uuid::new_v4();
let tar_bytes = create_codebase_tar(main_js);
place_codebase_in_cache(&job_id, &tar_bytes, false);
let job = JobPayload::Code(RawCode {
hash: Some(-43), // PREVIEW_IS_TAR_CODEBASE_HASH
content: inner_content.to_string(),
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: Default::default(),
debouncing_settings: Default::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
});
let result = RunJob::from(job)
.job_id(job_id)
.run_until_complete(&db, false, port)
.await
.json_result()
.unwrap();
assert_eq!(result, serde_json::json!("cjs codebase ok"));
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_esm_codebase_tar(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let main_js = r#"
export function main() {
return "esm codebase ok";
}
"#;
let inner_content = r#"export function main() { return "esm codebase ok"; }"#;
let job_id = Uuid::new_v4();
let tar_bytes = create_codebase_tar(main_js);
place_codebase_in_cache(&job_id, &tar_bytes, true);
let job = JobPayload::Code(RawCode {
hash: Some(-45), // PREVIEW_IS_TAR_ESM_CODEBASE_HASH
content: inner_content.to_string(),
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: Default::default(),
debouncing_settings: Default::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
});
let result = RunJob::from(job)
.job_id(job_id)
.run_until_complete(&db, false, port)
.await
.json_result()
.unwrap();
assert_eq!(result, serde_json::json!("esm codebase ok"));
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_cjs_codebase_tar_nsjail(db: Pool<Postgres>) -> anyhow::Result<()> {
if std::process::Command::new("nsjail")
.arg("--help")
.output()
.is_err()
{
eprintln!("nsjail not found, skipping test");
return Ok(());
}
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let main_js = r#"
module.exports.main = function() {
return "cjs nsjail ok";
};
"#;
let inner_content = r#"export function main() { return "cjs nsjail ok"; }"#;
let job_id = Uuid::new_v4();
let tar_bytes = create_codebase_tar(main_js);
place_codebase_in_cache(&job_id, &tar_bytes, false);
let job = JobPayload::Code(RawCode {
hash: Some(-43),
content: inner_content.to_string(),
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: Default::default(),
debouncing_settings: Default::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
});
use std::sync::atomic::Ordering;
windmill_worker::JOB_ISOLATION.store(
windmill_worker::JobIsolationLevel::NsjailSandboxing as u8,
Ordering::Relaxed,
);
let result = RunJob::from(job)
.job_id(job_id)
.run_until_complete(&db, false, port)
.await;
windmill_worker::JOB_ISOLATION.store(
windmill_worker::JobIsolationLevel::Undefined as u8,
Ordering::Relaxed,
);
let json = result.json_result().unwrap();
assert_eq!(json, serde_json::json!("cjs nsjail ok"));
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_esm_codebase_tar_nsjail(db: Pool<Postgres>) -> anyhow::Result<()> {
if std::process::Command::new("nsjail")
.arg("--help")
.output()
.is_err()
{
eprintln!("nsjail not found, skipping test");
return Ok(());
}
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let main_js = r#"
export function main() {
return "esm nsjail ok";
}
"#;
let inner_content = r#"export function main() { return "esm nsjail ok"; }"#;
let job_id = Uuid::new_v4();
let tar_bytes = create_codebase_tar(main_js);
place_codebase_in_cache(&job_id, &tar_bytes, true);
let job = JobPayload::Code(RawCode {
hash: Some(-45),
content: inner_content.to_string(),
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: Default::default(),
debouncing_settings: Default::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
});
use std::sync::atomic::Ordering;
windmill_worker::JOB_ISOLATION.store(
windmill_worker::JobIsolationLevel::NsjailSandboxing as u8,
Ordering::Relaxed,
);
let result = RunJob::from(job)
.job_id(job_id)
.run_until_complete(&db, false, port)
.await;
windmill_worker::JOB_ISOLATION.store(
windmill_worker::JobIsolationLevel::Undefined as u8,
Ordering::Relaxed,
);
let json = result.json_result().unwrap();
assert_eq!(json, serde_json::json!("esm nsjail ok"));
Ok(())
}

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.651.1
version: 1.650.0
title: Windmill API
contact:

View File

@@ -64,10 +64,9 @@ impl McpBackend for WindmillBackend {
auth: &ApiAuthed,
workspace_id: &str,
favorites_only: bool,
path_prefix: Option<&str>,
) -> BackendResult<Vec<ScriptInfo>> {
let scope_type = if favorites_only { "favorites" } else { "all" };
get_items::<ScriptInfo>(&self.user_db, auth, workspace_id, scope_type, "script", path_prefix)
get_items::<ScriptInfo>(&self.user_db, auth, workspace_id, scope_type, "script")
.await
.map_err(|e| ErrorData::internal_error(e.message, None))
}
@@ -77,10 +76,9 @@ impl McpBackend for WindmillBackend {
auth: &ApiAuthed,
workspace_id: &str,
favorites_only: bool,
path_prefix: Option<&str>,
) -> BackendResult<Vec<FlowInfo>> {
let scope_type = if favorites_only { "favorites" } else { "all" };
get_items::<FlowInfo>(&self.user_db, auth, workspace_id, scope_type, "flow", path_prefix)
get_items::<FlowInfo>(&self.user_db, auth, workspace_id, scope_type, "flow")
.await
.map_err(|e| ErrorData::internal_error(e.message, None))
}

View File

@@ -136,7 +136,6 @@ pub async fn get_items<T: for<'a> sqlx::FromRow<'a, sqlx::postgres::PgRow> + Sen
workspace_id: &str,
scope_type: &str,
item_type: &str,
path_prefix: Option<&str>,
) -> Result<Vec<T>, ErrorData> {
let mut sqlb = SqlBuilder::select_from(&format!("{} as o", item_type));
let fields = vec!["o.path", "o.summary", "o.description", "o.schema"];
@@ -154,11 +153,6 @@ pub async fn get_items<T: for<'a> sqlx::FromRow<'a, sqlx::postgres::PgRow> + Sen
sqlb.and_where("(o.no_main_func IS NOT TRUE OR o.no_main_func IS NULL)");
}
if let Some(prefix) = path_prefix {
let escaped = prefix.replace('\\', "\\\\").replace('%', "\\%").replace('_', "\\_");
sqlb.and_where("o.path LIKE ? ESCAPE '\\'".bind(&format!("{}%", escaped)));
}
sqlb.order_by(
if item_type == "flow" {
"o.edited_at"

View File

@@ -11,8 +11,6 @@ pub mod types;
pub use schema::convert_schema_to_schema_type;
pub use scope::{is_resource_allowed, parse_mcp_scopes, McpScopeConfig};
pub use transform::{
apply_key_transformation, extract_hub_version_id_from_hashed,
extract_path_prefix_from_hashed, parse_tool_prefix, reverse_transform, reverse_transform_key,
transform_hub_path, transform_path,
apply_key_transformation, reverse_transform, reverse_transform_key, transform_path,
};
pub use types::*;

View File

@@ -4,15 +4,9 @@
//! to make them compatible with MCP tool naming requirements.
use super::types::SchemaType;
use windmill_common::utils::calculate_hash;
/// Max tool name length. The MCP spec allows 64 chars, but some clients
/// (e.g. Cursor) prepend the server name to the tool name, so we use 40
/// to leave room for that prefix.
const MAX_PATH_LENGTH: usize = 40;
/// Length of the SHA256 hash suffix used for hashed names
const HASH_LEN: usize = 16;
/// MCP clients do not allow names longer than 60 characters
const MAX_PATH_LENGTH: usize = 60;
/// Transform the path for workspace scripts/flows
///
@@ -20,133 +14,19 @@ const HASH_LEN: usize = 16;
/// path with the type prefix. This is used when listing, because we can't
/// have names with slashes. Because we replace slashes with underscores,
/// we also need to escape underscores.
///
/// For short names (≤40 chars): `s-{escaped_path}` or `f-{escaped_path}`
/// For long names (>40 chars): `S-{escaped[:22]}{sha256[:16]}` or `F-{escaped[:22]}{sha256[:16]}`
///
/// The uppercase prefix signals that the name is hashed.
pub fn transform_path(path: &str, type_str: &str) -> String {
let escaped_path = path.replace('_', "__").replace('/', "_");
let prefix_char = &type_str[..1];
let short_name = format!("{}-{}", prefix_char, escaped_path);
if short_name.len() <= MAX_PATH_LENGTH {
return short_name;
// first letter of type_str is used as prefix, only one letter to avoid reaching 60 char name limit
let transformed_path = format!("{}-{}", &type_str[..1], escaped_path);
if transformed_path.len() > MAX_PATH_LENGTH {
let suffix = "_TRUNC";
return format!(
"{}{}",
&transformed_path[..MAX_PATH_LENGTH - suffix.len()],
suffix
);
}
let upper_prefix = prefix_char.to_uppercase();
// Layout: "{Upper}-" (2 chars) + prefix_body (22 chars) + hash (16 chars) = 40
let prefix_body_len = MAX_PATH_LENGTH - 2 - HASH_LEN;
let hash = calculate_hash(&short_name);
let hash_suffix = &hash[..HASH_LEN];
let truncated = truncate_to_char_boundary(&escaped_path, prefix_body_len);
format!("{}-{}{}", upper_prefix, truncated, hash_suffix)
}
/// Transform the path for hub scripts
///
/// For short names (≤40 chars): `hs-{id}-{summary}`
/// For long names (>40 chars): `Hs-{id}-{summary[:N]}{sha256[:16]}`
pub fn transform_hub_path(version_id: u64, summary: &str) -> String {
let escaped_summary = summary.replace(' ', "_");
let short_name = format!("hs-{}-{}", version_id, escaped_summary);
if short_name.len() <= MAX_PATH_LENGTH {
return short_name;
}
let hash = calculate_hash(&short_name);
let hash_suffix = &hash[..HASH_LEN];
// "Hs-{id}-" prefix, then fill remaining with summary + hash
let fixed_prefix = format!("Hs-{}-", version_id);
let available = MAX_PATH_LENGTH - fixed_prefix.len() - HASH_LEN;
let truncated_summary = truncate_to_char_boundary(&escaped_summary, available);
format!("{}{}{}", fixed_prefix, truncated_summary, hash_suffix)
}
/// Parse the prefix of any tool name (both short and hashed).
/// Returns `(type_str, is_hub, is_hashed)`.
/// Hashed names use an uppercase first character as the signal.
pub fn parse_tool_prefix(name: &str) -> Result<(&str, bool, bool), String> {
let is_hashed = name.chars().next().map(|c| c.is_ascii_uppercase()).unwrap_or(false);
let lower = name.to_ascii_lowercase();
let (type_str, is_hub) = if lower.starts_with("hs-") {
("script", true)
} else if lower.starts_with("s-") {
("script", false)
} else if lower.starts_with("f-") {
("flow", false)
} else {
return Err(format!("Invalid tool name prefix: {}", name));
};
Ok((type_str, is_hub, is_hashed))
}
/// Extract the hub version_id from a hashed hub script name like `Hs-{id}-...`
pub fn extract_hub_version_id_from_hashed(name: &str) -> Result<String, String> {
let rest = name
.strip_prefix("Hs-")
.ok_or_else(|| format!("Not a hashed hub name: {}", name))?;
let id = rest
.split('-')
.next()
.ok_or_else(|| format!("No version_id in hashed hub name: {}", name))?;
if id.is_empty() {
return Err(format!("Empty version_id in hashed hub name: {}", name));
}
Ok(id.to_string())
}
/// Extract a safe original-path prefix from a hashed tool name.
///
/// Given `S-u_admin_engineering__te<hash16>`, extracts the escaped prefix between
/// the type prefix (`S-`, `F-`, or `Hs-`) and the hash, un-escapes it, and
/// returns a prefix suitable for `WHERE path LIKE '{prefix}%'`.
///
/// Returns `None` if the name is too short or has an unrecognized prefix.
pub fn extract_path_prefix_from_hashed(name: &str) -> Option<String> {
let prefix_len = if name.starts_with("Hs-") {
3
} else if name.starts_with("S-") || name.starts_with("F-") {
2
} else {
return None;
};
if name.len() <= prefix_len + HASH_LEN {
return None;
}
let escaped_prefix = &name[prefix_len..name.len() - HASH_LEN];
if escaped_prefix.is_empty() {
return None;
}
// Strip trailing underscores — they may be half of a `__` pair split by truncation
let trimmed = escaped_prefix.trim_end_matches('_');
if trimmed.is_empty() {
return None;
}
Some(unescape_path(trimmed))
}
/// Un-escape a mangled path segment: `__` → `_`, standalone `_` → `/`.
fn unescape_path(s: &str) -> String {
const TEMP_PLACEHOLDER: &str = "@@UNDERSCORE@@";
s.replace("__", TEMP_PLACEHOLDER)
.replace('_', "/")
.replace(TEMP_PLACEHOLDER, "_")
}
/// Truncate a string to at most `max_len` bytes, ensuring we don't split a UTF-8 character.
fn truncate_to_char_boundary(s: &str, max_len: usize) -> &str {
if s.len() <= max_len {
return s;
}
let mut end = max_len;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
&s[..end]
transformed_path
}
/// Reverse the transformation of a path
@@ -158,22 +38,25 @@ fn truncate_to_char_boundary(s: &str, max_len: usize) -> &str {
/// This is used in call_tool to get the original path, and the type of the item.
///
/// Returns: (type, original_path, is_hub)
///
/// Note: This only works for non-hashed (short) names. Hashed names must be
/// resolved via `parse_tool_prefix` + path enumeration in the runner.
pub fn reverse_transform(transformed_path: &str) -> Result<(&str, String, bool), String> {
let (type_str, is_hub, is_hashed) = parse_tool_prefix(transformed_path)?;
let is_hub = transformed_path.starts_with("h");
let transformed_path = if is_hub {
transformed_path[1..].to_string()
} else {
transformed_path.to_string()
};
let type_str = if transformed_path.starts_with("s-") {
"script"
} else if transformed_path.starts_with("f-") {
"flow"
} else {
return Err(format!(
"Invalid prefix in transformed path: {}",
transformed_path
));
};
if is_hashed {
return Err(
"Hashed names cannot be reverse-transformed directly; use path enumeration instead"
.to_string(),
);
}
// Strip the prefix: "hs-" (3 chars) for hub, "s-"/"f-" (2 chars) for others
let prefix_len = if is_hub { 3 } else { 2 };
let mangled_path = &transformed_path[prefix_len..];
let mangled_path = &transformed_path[2..];
let original_path = if is_hub {
let parts = mangled_path.split("-").collect::<Vec<&str>>();
@@ -182,7 +65,11 @@ pub fn reverse_transform(transformed_path: &str) -> Result<(&str, String, bool),
}
parts[0].to_string()
} else {
unescape_path(mangled_path)
const TEMP_PLACEHOLDER: &str = "@@UNDERSCORE@@";
mangled_path
.replace("__", TEMP_PLACEHOLDER)
.replace('_', "/")
.replace(TEMP_PLACEHOLDER, "_")
};
Ok((type_str, original_path, is_hub))
@@ -210,13 +97,16 @@ pub fn reverse_transform_key(transformed_key: &str, schema_obj: &Option<SchemaTy
let schema_obj = match schema_obj {
Some(s) => s,
None => {
// No schema available, return the key as is (best guess)
return transformed_key.to_string();
}
};
for original_key_in_schema in schema_obj.properties.keys() {
// Apply the SAME forward transformation to the schema key
let potential_transformed_key = apply_key_transformation(original_key_in_schema);
// If it matches the key we received, we found the likely original
if potential_transformed_key == transformed_key {
return original_key_in_schema.clone();
}
@@ -230,7 +120,7 @@ mod tests {
use super::*;
#[test]
fn test_transform_path_short() {
fn test_transform_path() {
assert_eq!(
transform_path("u/admin/script", "script"),
"s-u_admin_script"
@@ -240,108 +130,7 @@ mod tests {
}
#[test]
fn test_transform_path_long_is_hashed() {
let long_path = "u/engineering/team/automation/very_long_script_name_that_exceeds_limit";
let result = transform_path(long_path, "script");
assert_eq!(result.len(), MAX_PATH_LENGTH);
assert!(result.starts_with("S-"));
let (_, _, is_hashed) = parse_tool_prefix(&result).unwrap();
assert!(is_hashed);
}
#[test]
fn test_transform_path_long_flow_is_hashed() {
let long_path = "f/engineering/team/automation/very_long_flow_name_that_exceeds_limit";
let result = transform_path(long_path, "flow");
assert_eq!(result.len(), MAX_PATH_LENGTH);
assert!(result.starts_with("F-"));
let (_, _, is_hashed) = parse_tool_prefix(&result).unwrap();
assert!(is_hashed);
}
#[test]
fn test_transform_path_hashing_is_deterministic() {
let path = "u/engineering/team/automation/very_long_script_name_that_exceeds_limit";
let a = transform_path(path, "script");
let b = transform_path(path, "script");
assert_eq!(a, b);
}
#[test]
fn test_transform_path_different_long_paths_differ() {
let a = transform_path(
"u/engineering/team/automation/very_long_script_name_that_exceeds_limit_a",
"script",
);
let b = transform_path(
"u/engineering/team/automation/very_long_script_name_that_exceeds_limit_b",
"script",
);
assert_ne!(a, b);
}
#[test]
fn test_transform_hub_path_short() {
let result = transform_hub_path(12345, "Send Slack Message");
assert_eq!(result, "hs-12345-Send_Slack_Message");
let (_, _, is_hashed) = parse_tool_prefix(&result).unwrap();
assert!(!is_hashed);
}
#[test]
fn test_transform_hub_path_long_is_hashed() {
let result = transform_hub_path(
12345,
"Send Slack Message To Channel With Very Long Description That Exceeds Limit",
);
assert_eq!(result.len(), MAX_PATH_LENGTH);
assert!(result.starts_with("Hs-12345-"));
let (_, _, is_hashed) = parse_tool_prefix(&result).unwrap();
assert!(is_hashed);
}
#[test]
fn test_extract_hub_version_id_from_hashed() {
let name = "Hs-12345-Send_Slack_Message_To_Ch9e8d7c6b5a4f3e2d";
let id = extract_hub_version_id_from_hashed(name).unwrap();
assert_eq!(id, "12345");
}
#[test]
fn test_parse_tool_prefix() {
let (t, hub, hashed) = parse_tool_prefix("S-something").unwrap();
assert_eq!(t, "script");
assert!(!hub);
assert!(hashed);
let (t, hub, hashed) = parse_tool_prefix("F-something").unwrap();
assert_eq!(t, "flow");
assert!(!hub);
assert!(hashed);
let (t, hub, hashed) = parse_tool_prefix("Hs-12345-something").unwrap();
assert_eq!(t, "script");
assert!(hub);
assert!(hashed);
let (t, hub, hashed) = parse_tool_prefix("s-u_admin_script").unwrap();
assert_eq!(t, "script");
assert!(!hub);
assert!(!hashed);
let (t, hub, hashed) = parse_tool_prefix("f-f_folder_flow").unwrap();
assert_eq!(t, "flow");
assert!(!hub);
assert!(!hashed);
let (t, hub, hashed) = parse_tool_prefix("hs-12345-summary").unwrap();
assert_eq!(t, "script");
assert!(hub);
assert!(!hashed);
}
#[test]
fn test_reverse_transform_short_names() {
fn test_reverse_transform() {
let (type_str, path, is_hub) = reverse_transform("s-u_admin_script").unwrap();
assert_eq!(type_str, "script");
assert_eq!(path, "u/admin/script");
@@ -353,70 +142,6 @@ mod tests {
assert!(!is_hub);
}
#[test]
fn test_extract_path_prefix_from_hashed() {
// Generate a real hashed name and verify prefix extraction
let long_path = "u/admin/engineering/team/automation/very_long_script";
let hashed = transform_path(long_path, "script");
let (_, _, is_hashed) = parse_tool_prefix(&hashed).unwrap();
assert!(is_hashed);
let prefix = extract_path_prefix_from_hashed(&hashed).unwrap();
// The original path should start with the extracted prefix
assert!(
long_path.starts_with(&prefix),
"path '{}' should start with prefix '{}'",
long_path,
prefix
);
}
#[test]
fn test_extract_path_prefix_underscore_in_path() {
let long_path = "u/admin/my_team/automation/very_long_script_name_here";
let hashed = transform_path(long_path, "script");
let prefix = extract_path_prefix_from_hashed(&hashed).unwrap();
assert!(
long_path.starts_with(&prefix),
"path '{}' should start with prefix '{}'",
long_path,
prefix
);
}
#[test]
fn test_extract_path_prefix_rejects_invalid_prefix() {
assert!(extract_path_prefix_from_hashed("x-something").is_none());
assert!(extract_path_prefix_from_hashed("").is_none());
assert!(extract_path_prefix_from_hashed("S-").is_none());
}
#[test]
fn test_extract_path_prefix_handles_hs_prefix() {
// Hs- is 3 chars, not 2 — ensure the prefix is stripped correctly
let hashed = transform_hub_path(12345, "a]very long hub script summary that exceeds the limit");
let (_, is_hub, is_hashed) = parse_tool_prefix(&hashed).unwrap();
assert!(is_hub);
assert!(is_hashed);
let prefix = extract_path_prefix_from_hashed(&hashed);
// Should not start with 's' (leftover from Hs- if sliced at index 2)
if let Some(ref p) = prefix {
assert!(
!p.starts_with('s'),
"prefix '{}' should not start with 's' from mis-sliced Hs- prefix",
p
);
}
}
#[test]
fn test_reverse_transform_rejects_hashed_names() {
assert!(reverse_transform("S-something").is_err());
assert!(reverse_transform("F-something").is_err());
assert!(reverse_transform("Hs-12345-something").is_err());
}
#[test]
fn test_apply_key_transformation() {
assert_eq!(apply_key_transformation("my key"), "my_key");

View File

@@ -92,10 +92,8 @@ pub struct ItemSchema {
/// Trait for objects that can be converted to MCP tools
pub trait ToolableItem {
/// Get the MCP-compatible tool name (path transformed with escaping/hashing)
fn get_transformed_path(&self) -> String;
/// Get the original full path of this item (for display in tool title)
fn get_full_path(&self) -> &str;
/// Get the path or identifier for this item (transformed for MCP compatibility)
fn get_path_or_id(&self) -> String;
/// Get the summary/title of this item
fn get_summary(&self) -> &str;
/// Get the description of this item

View File

@@ -14,9 +14,9 @@ pub mod client;
// Re-export common types at crate root for convenience
pub use common::{
convert_schema_to_schema_type, is_resource_allowed, parse_mcp_scopes, transform_hub_path,
transform_path, FlowInfo, HubResponse, HubScriptInfo, ItemSchema, McpScopeConfig, ResourceInfo,
ResourceType, SchemaType, ScriptInfo, ToolableItem, WorkspaceId,
convert_schema_to_schema_type, is_resource_allowed, parse_mcp_scopes, transform_path, FlowInfo,
HubResponse, HubScriptInfo, ItemSchema, McpScopeConfig, ResourceInfo, ResourceType, SchemaType,
ScriptInfo, ToolableItem, WorkspaceId,
};
// Re-export client types at crate root for backward compatibility

View File

@@ -55,22 +55,20 @@ pub trait McpBackend: Send + Sync + Clone + 'static {
// Listing Operations
// ─────────────────────────────────────────────────────────────────
/// List scripts, optionally filtered to favorites only and/or by path prefix
/// List scripts, optionally filtered to favorites only
async fn list_scripts(
&self,
auth: &Self::Auth,
workspace_id: &str,
favorites_only: bool,
path_prefix: Option<&str>,
) -> BackendResult<Vec<ScriptInfo>>;
/// List flows, optionally filtered to favorites only and/or by path prefix
/// List flows, optionally filtered to favorites only
async fn list_flows(
&self,
auth: &Self::Auth,
workspace_id: &str,
favorites_only: bool,
path_prefix: Option<&str>,
) -> BackendResult<Vec<FlowInfo>>;
/// List resource types in workspace

View File

@@ -5,10 +5,7 @@
use crate::common::schema::extract_resource_types_from_schema;
use crate::common::scope::parse_mcp_scopes;
use crate::common::transform::{
extract_hub_version_id_from_hashed, extract_path_prefix_from_hashed, parse_tool_prefix,
reverse_transform, reverse_transform_key,
};
use crate::common::transform::{reverse_transform, reverse_transform_key};
use crate::common::types::{ResourceInfo, ToolableItem, WorkspaceId};
use crate::server::backend::{McpAuth, McpBackend};
use crate::server::endpoints::endpoint_tool_to_mcp_tool;
@@ -84,13 +81,6 @@ impl<B: McpBackend> Runner<B> {
}
}
fn find_matching_path<T: ToolableItem>(candidates: Vec<T>, request_name: &str) -> Option<String> {
candidates
.into_iter()
.find(|item| item.get_transformed_path() == request_name)
.map(|item| item.get_full_path().to_string())
}
impl<B: McpBackend> ServerHandler for Runner<B> {
fn get_info(&self) -> ServerInfo {
ServerInfo {
@@ -130,9 +120,9 @@ impl<B: McpBackend> ServerHandler for Runner<B> {
// Fetch all items concurrently
let (scripts, flows, resource_types, hub_scripts) = tokio::try_join!(
self.backend
.list_scripts(&auth, &workspace_id, favorites_only, None),
.list_scripts(&auth, &workspace_id, favorites_only),
self.backend
.list_flows(&auth, &workspace_id, favorites_only, None),
.list_flows(&auth, &workspace_id, favorites_only),
self.backend.list_resource_types(&auth, &workspace_id),
async {
if let Some(ref apps) = scope_config.hub_apps {
@@ -241,6 +231,17 @@ impl<B: McpBackend> ServerHandler for Runner<B> {
let scope_config =
parse_mcp_scopes(scopes).map_err(|e| ErrorData::internal_error(e, None))?;
// Handle truncated tool names
if request.name.ends_with("_TRUNC") {
return Ok(CallToolResult::error(vec![rmcp::model::Annotated::new(
rmcp::model::RawContent::Text(rmcp::model::RawTextContent {
text: "Tool path is too long. Consider shortening it to make it compatible with MCP.".to_string(),
meta: None,
}),
None,
)]));
}
let args = request.arguments.map(Value::Object).unwrap_or(Value::Null);
// Check if this is an endpoint tool
@@ -273,58 +274,10 @@ impl<B: McpBackend> ServerHandler for Runner<B> {
}
}
// Resolve the tool name to (type, path, is_hub)
let (type_str, is_hub, is_hashed) =
parse_tool_prefix(&request.name).map_err(|e| {
ErrorData::internal_error(format!("Failed to parse tool name: {}", e), None)
})?;
let (tool_type, path, is_hub) = if !is_hashed {
reverse_transform(&request.name).map_err(|e| {
ErrorData::internal_error(format!("Failed to parse tool name: {}", e), None)
})?
} else if is_hub {
let version_id =
extract_hub_version_id_from_hashed(&request.name).map_err(|e| {
ErrorData::internal_error(
format!("Failed to extract hub version_id: {}", e),
None,
)
})?;
(type_str, version_id, true)
} else {
let path_prefix = extract_path_prefix_from_hashed(&request.name);
let favorites_only = scope_config.favorites;
let matched_path = if type_str == "script" {
find_matching_path(
self.backend
.list_scripts(&auth, &workspace_id, favorites_only, path_prefix.as_deref())
.await
.map_err(|e| ErrorData::internal_error(e.message, None))?,
&request.name,
)
} else {
find_matching_path(
self.backend
.list_flows(&auth, &workspace_id, favorites_only, path_prefix.as_deref())
.await
.map_err(|e| ErrorData::internal_error(e.message, None))?,
&request.name,
)
};
let matched_path = matched_path.ok_or_else(|| {
ErrorData::internal_error(
format!(
"No {} found matching hashed tool name '{}'",
type_str, request.name
),
None,
)
})?;
(type_str, matched_path, false)
};
// Not an endpoint tool - parse as script/flow
let (tool_type, path, is_hub) = reverse_transform(&request.name).map_err(|e| {
ErrorData::internal_error(format!("Failed to parse tool name: {}", e), None)
})?;
// Validate script/flow scope
if !is_hub && scope_config.granular {

View File

@@ -9,7 +9,7 @@ use std::collections::HashMap;
use std::sync::Arc;
use crate::common::schema::{convert_schema_to_schema_type, make_schema_compatible};
use crate::common::transform::{transform_hub_path, transform_path};
use crate::common::transform::transform_path;
use crate::common::types::{
FlowInfo, HubScriptInfo, ResourceInfo, ResourceType, SchemaType, ScriptInfo, ToolableItem,
};
@@ -17,14 +17,10 @@ use crate::server::backend::McpBackend;
/// Implementation of ToolableItem for ScriptInfo
impl ToolableItem for ScriptInfo {
fn get_transformed_path(&self) -> String {
fn get_path_or_id(&self) -> String {
transform_path(&self.path, "script")
}
fn get_full_path(&self) -> &str {
&self.path
}
fn get_summary(&self) -> &str {
self.summary.as_deref().unwrap_or("No summary")
}
@@ -52,14 +48,10 @@ impl ToolableItem for ScriptInfo {
/// Implementation of ToolableItem for FlowInfo
impl ToolableItem for FlowInfo {
fn get_transformed_path(&self) -> String {
fn get_path_or_id(&self) -> String {
transform_path(&self.path, "flow")
}
fn get_full_path(&self) -> &str {
&self.path
}
fn get_summary(&self) -> &str {
self.summary.as_deref().unwrap_or("No summary")
}
@@ -87,13 +79,10 @@ impl ToolableItem for FlowInfo {
/// Implementation of ToolableItem for HubScriptInfo
impl ToolableItem for HubScriptInfo {
fn get_transformed_path(&self) -> String {
fn get_path_or_id(&self) -> String {
let id = self.version_id;
let summary = self.summary.as_deref().unwrap_or("No summary");
transform_hub_path(self.version_id, summary)
}
fn get_full_path(&self) -> &str {
self.summary.as_deref().unwrap_or("No summary")
format!("hs-{}-{}", id, summary.replace(" ", "_"))
}
fn get_summary(&self) -> &str {
@@ -135,7 +124,7 @@ pub fn create_tool_from_item<T: ToolableItem, B: McpBackend>(
resources_types: &[ResourceType],
) -> Tool {
let is_hub = item.is_hub();
let path = item.get_transformed_path();
let path = item.get_path_or_id();
let item_type = item.item_type();
let description = format!(
"This is a {} named `{}` with the following description: `{}`.{}",
@@ -181,24 +170,15 @@ pub fn create_tool_from_item<T: ToolableItem, B: McpBackend>(
}
};
let title = {
let summary = item.get_summary();
if summary == "No summary" {
item.get_full_path().to_string()
} else {
summary.to_string()
}
};
Tool {
name: Cow::Owned(path),
description: Some(Cow::Owned(description)),
input_schema: Arc::new(input_schema_map),
title: Some(title.clone()),
title: Some(item.get_summary().to_string()),
output_schema: None,
icons: None,
annotations: Some(ToolAnnotations {
title: Some(title),
title: Some(item.get_summary().to_string()),
read_only_hint: Some(false), // Can modify environment
destructive_hint: Some(true), // Can potentially be destructive
idempotent_hint: Some(false), // Are not guaranteed to be idempotent

View File

@@ -536,6 +536,13 @@ async fn delete_variable(
)
.execute(&mut *tx)
.await?;
sqlx::query!(
"DELETE FROM resource WHERE path = $1 AND workspace_id = $2",
path,
w_id
)
.execute(&mut *tx)
.await?;
audit_log(
&mut *tx,
&authed,

View File

@@ -151,7 +151,6 @@ pub struct RunJob {
pub args: serde_json::Map<String, serde_json::Value>,
pub scheduled_for_o: Option<chrono::DateTime<chrono::Utc>>,
pub email: String,
pub job_id: Option<Uuid>,
}
impl From<JobPayload> for RunJob {
@@ -161,7 +160,6 @@ impl From<JobPayload> for RunJob {
args: Default::default(),
scheduled_for_o: None,
email: "test@windmill.dev".to_string(),
job_id: None,
}
}
}
@@ -185,13 +183,8 @@ impl RunJob {
self
}
pub fn job_id(mut self, id: Uuid) -> Self {
self.job_id = Some(id);
self
}
pub async fn push(self, db: &Pool<Postgres>) -> Uuid {
let RunJob { payload, args, scheduled_for_o, email, job_id } = self;
let RunJob { payload, args, scheduled_for_o, email } = self;
let mut hm_args = std::collections::HashMap::new();
for (k, v) in args {
hm_args.insert(k, windmill_common::worker::to_raw_value(&v));
@@ -213,7 +206,7 @@ impl RunJob {
/* parent_job */ None,
/* root job */ None,
/* flow_innermost_root_job */ None,
/* job_id */ job_id,
/* job_id */ None,
/* is_flow_step */ false,
/* same_worker */ false,
None,
@@ -237,7 +230,7 @@ impl RunJob {
/// Push the job as a specific user (for testing permissions)
pub async fn push_as(self, db: &Pool<Postgres>, username: &str, email: &str) -> Uuid {
let RunJob { payload, args, scheduled_for_o, job_id, .. } = self;
let RunJob { payload, args, scheduled_for_o, .. } = self;
let mut hm_args = std::collections::HashMap::new();
for (k, v) in args {
hm_args.insert(k, windmill_common::worker::to_raw_value(&v));
@@ -259,7 +252,7 @@ impl RunJob {
/* parent_job */ None,
/* root job */ None,
/* flow_innermost_root_job */ None,
/* job_id */ job_id,
/* job_id */ None,
/* is_flow_step */ false,
/* same_worker */ false,
None,

View File

@@ -1036,12 +1036,6 @@ pub async fn handle_bun_job(
let _ = write_file(job_dir, "main.ts", inner_content)?;
} else if !annotation.native && codebase.is_none() {
let _ = write_file(job_dir, "package.json", r#"{ "type": "module" }"#)?;
} else if codebase.is_some() {
// Write a valid fallback package.json for codebase mode. Without this,
// nsjail creates an empty 0-byte file (from the mandatory: false mount)
// which Node.js fails to parse as JSON (ERR_INVALID_PACKAGE_CONFIG).
// If the codebase TAR includes a package.json, it will overwrite this.
let _ = write_file(job_dir, "package.json", "{}")?;
};
let common_bun_proc_envs: HashMap<String, String> =

View File

@@ -4771,7 +4771,6 @@ mount {{
// Clean up absolute-path symlinks created by setup_volume_mount_paths
if !is_sandboxing_enabled() {
#[allow(unused_variables)] // state is only used on unix
for state in &volume_setup.states {
#[cfg(unix)]
if state.mount.target.starts_with('/') {

View File

@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.651.1";
export const VERSION = "v1.650.0";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({

View File

@@ -39,10 +39,7 @@ export const scriptBootstrapCode = {
}
`,
bun: `// there are multiple modes to add as header: //nobundling //native //npm //nodejs
// https://www.windmill.dev/docs/getting_started/scripts_quickstart/typescript#modes
export async function main() {
bun: `export async function main() {
return "Hello world";
}
`,

View File

@@ -67,7 +67,7 @@ export {
workspaceAdd,
};
export const VERSION = "1.651.1";
export const VERSION = "1.650.0";
// Re-exported from constants.ts to maintain backwards compatibility
export { WM_FORK_PREFIX } from "./core/constants.ts";

View File

@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.651.1",
"version": "1.650.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.651.1",
"version": "1.650.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.651.1",
"version": "1.650.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",

View File

@@ -133,7 +133,7 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) {
])
})
}
promise = CancelablePromiseUtils.finallyDo(promise, () => {
promise = CancelablePromiseUtils.pipe(promise, () => {
if (slowStreamIntervalId) {
clearInterval(slowStreamIntervalId)
slowStreamIntervalId = undefined
@@ -161,7 +161,7 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) {
)
}, 15000)
paramChangePromise = loadJobsIntern(false, size)
paramChangePromise = CancelablePromiseUtils.finallyDo(paramChangePromise, () => {
paramChangePromise = CancelablePromiseUtils.pipe(paramChangePromise, () => {
if (slowStreamIntervalId) {
clearInterval(slowStreamIntervalId)
slowStreamIntervalId = undefined
@@ -687,10 +687,6 @@ export function useJobsLoader(args: () => UseJobLoaderArgs) {
return () => {
clearTimeout(paramChangeTimeout)
paramChangePromise?.cancel()
if (slowStreamIntervalId) {
clearInterval(slowStreamIntervalId)
slowStreamIntervalId = undefined
}
}
})
$effect(() => {

View File

@@ -17,7 +17,7 @@
import { safeSelectItems } from '../select/utils.svelte'
import TokenDisplay from './TokenDisplay.svelte'
import ScopeSelector from './ScopeSelector.svelte'
import Alert from '../common/alert/Alert.svelte'
import FolderPicker from '../FolderPicker.svelte'
import TextInput from '../text_input/TextInput.svelte'
import Select from '../select/Select.svelte'
@@ -43,6 +43,8 @@
displayCreateToken = true
}: Props = $props()
// MCP clients do not allow names longer than 60 characters, here we use 55 because final tool name server side will add ~5 characters
const MAX_PATH_LENGTH = 55
let newToken = $state<string | undefined>(undefined)
let newMcpToken = $state<string | undefined>(undefined)
@@ -183,6 +185,17 @@
? `You do not have any favorite scripts or flows. You can favorite some scripts and flows to include them, or change the scope to "All scripts/flows" to include all your scripts and flows.`
: `You do not have any scripts or flows in the selected folder.`
)
const longPathRunnables = $derived(
includedRunnables.filter((path) => path.length > MAX_PATH_LENGTH)
)
const validRunnables = $derived(
includedRunnables.filter((path) => path.length <= MAX_PATH_LENGTH)
)
const longPathWarning = $derived(
longPathRunnables.length > 0
? `${longPathRunnables.length} script(s)/flow(s) have paths longer than 60 characters and will be excluded from MCP tools. Consider shortening the paths: ${longPathRunnables.slice(0, 3).join(', ')}${longPathRunnables.length > 3 ? ` and ${longPathRunnables.length - 3} more` : ''}`
: ''
)
$effect(() => {
if (mcpCreationMode) {
@@ -614,18 +627,23 @@
</div>
{:else}
<div class="flex flex-col gap-2 col-span-2 pr-4">
{#if longPathWarning}
<Alert type="warning" title="Some paths are too long" size="xs">
{longPathWarning}
</Alert>
{/if}
<span class="block text-xs">Scripts & Flows that will be available via MCP</span>
<div class="flex flex-wrap gap-1">
{#if includedRunnables.length > 0 && includedRunnables.length <= 5}
{#each includedRunnables as scriptOrFlow}
{#if validRunnables.length > 0 && validRunnables.length <= 5}
{#each validRunnables as scriptOrFlow}
<Badge rounded small color="blue">{scriptOrFlow}</Badge>
{/each}
{:else if includedRunnables.length > 0}
{#each includedRunnables.slice(0, 3) as scriptOrFlow}
{:else if validRunnables.length > 0}
{#each validRunnables.slice(0, 3) as scriptOrFlow}
<Badge rounded small color="blue">{scriptOrFlow}</Badge>
{/each}
<Badge rounded small color="dark-gray">
+{includedRunnables.length - 3} more
+{validRunnables.length - 3} more
</Badge>
{:else}
<p class="text-xs text-primary">

View File

@@ -4,7 +4,7 @@ verify_ssl = true
name = "pypi"
[packages]
wmill = ">=1.651.1"
wmill = ">=1.650.0"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"

View File

@@ -1,7 +1,7 @@
openapi: '3.0.3'
info:
version: 1.651.1
version: 1.650.0
title: OpenFlow Spec
contact:
name: Ruben Fiszel

View File

@@ -12,7 +12,7 @@
RootModule = 'WindmillClient.psm1'
# Version number of this module.
ModuleVersion = '1.651.1'
ModuleVersion = '1.650.0'
# Supported PSEditions
# CompatiblePSEditions = @()

View File

@@ -108,21 +108,6 @@ SET s3_secret_access_key='80yMndIMcyXwEujxVNINQbf0tBlIzRaLPyM2m1n4';
)
print(file_key)
@unittest.skip("skipping")
def test_delete_s3_object(self):
# Upload a temporary file
s3_obj = wmill.write_s3_file(
S3Object(s3="_wmill_test_delete_s3_object.txt"), b"delete_s3_object test content"
)
# Verify it exists
content = wmill.load_s3_file(s3_obj)
self.assertEqual(content, b"delete_s3_object test content")
# Delete it
wmill.delete_s3_object(s3_obj)
# Verify it's gone
with self.assertRaises(Exception):
wmill.load_s3_file(s3_obj)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
version = "1.651.1"
version = "1.650.0"
description = "A client library for accessing Windmill server wrapping the Windmill client API"
license = "Apache-2.0"
homepage = "https://windmill.dev"

View File

@@ -984,40 +984,6 @@ class Windmill:
raise Exception("Could not write file to S3") from e
return S3Object(s3=response["file_key"], storage=s3object.get("storage") if s3object else None)
def delete_s3_object(
self,
s3object: S3Object | str,
s3_resource_path: str | None = None,
) -> None:
"""
Permanently delete a file from the workspace S3 bucket.
'''python
from wmill import S3Object
s3_obj = S3Object(s3="/path/to/my_file.txt")
client.delete_s3_object(s3_obj)
'''
"""
s3object = parse_s3_object(s3object)
query_params: Dict[str, Any] = {"file_key": s3object["s3"]}
if s3_resource_path is not None and s3_resource_path != "":
query_params["s3_resource_path"] = s3_resource_path
if "storage" in s3object and s3object["storage"] is not None:
query_params["storage"] = s3object["storage"]
try:
resp = self.client.delete(
f"/w/{self.workspace}/job_helpers/delete_s3_file",
params=query_params,
)
resp.raise_for_status()
except httpx.HTTPStatusError as err:
error = f"{err.request.url}: {err.response.status_code}, {err.response.text}"
logger.error(error)
raise Exception(error)
except Exception as e:
raise Exception("Could not delete file from S3") from e
def sign_s3_objects(self, s3_objects: list[S3Object | str]) -> list[S3Object]:
"""Sign S3 objects for use by anonymous users in public apps.
@@ -1726,20 +1692,6 @@ def write_s3_file(
)
@init_global_client
def delete_s3_object(
s3object: S3Object | str,
s3_resource_path: str | None = None,
) -> None:
"""
Permanently delete a file from the workspace S3 bucket.
"""
return _client.delete_s3_object(
s3object,
s3_resource_path if s3_resource_path != "" else None,
)
@init_global_client
def sign_s3_objects(s3_objects: list[S3Object | str]) -> list[S3Object]:
"""
@@ -2416,4 +2368,4 @@ def parse_sql_client_name(name: str) -> tuple[str, Optional[str]]:
name, schema = name.split(":", 1)
if not name:
name = "main"
return name, schema
return name, schema

View File

@@ -1,6 +1,6 @@
{
"name": "@windmill/windmill",
"version": "1.651.1",
"version": "1.650.0",
"exports": "./src/index.ts",
"publish": {
"exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"]

View File

@@ -1,7 +1,7 @@
{
"name": "windmill-client",
"description": "Windmill SDK client for browsers and Node.js",
"version": "1.651.1",
"version": "1.650.0",
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"sideEffects": false,

View File

@@ -1 +1 @@
1.651.1
1.650.0