diff --git a/CHANGELOG.md b/CHANGELOG.md index ad25a9c096..0386e08fbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## [1.546.1](https://github.com/windmill-labs/windmill/compare/v1.546.0...v1.546.1) (2025-09-23) + + +### Bug Fixes + +* **mcp:** use stateless mode for openai sdk compatibility ([#6656](https://github.com/windmill-labs/windmill/issues/6656)) ([389b692](https://github.com/windmill-labs/windmill/commit/389b692523507a28916e96b481c60f3c49cd31da)) + +## [1.546.0](https://github.com/windmill-labs/windmill/compare/v1.545.0...v1.546.0) (2025-09-23) + + +### Features + +* app builder button tooltip ([#6652](https://github.com/windmill-labs/windmill/issues/6652)) ([08952c6](https://github.com/windmill-labs/windmill/commit/08952c6c6e0afdde8fc941f9f1d17870fe25878a)) +* dynamically hide tabs in app builder ([#6653](https://github.com/windmill-labs/windmill/issues/6653)) ([de7251d](https://github.com/windmill-labs/windmill/commit/de7251d85734757a1f3e222c715f807ba167d535)) +* split RUST_LOG into RUST_LOG and RUST_LOG_STDOUT ([7a13e9e](https://github.com/windmill-labs/windmill/commit/7a13e9e98840a456ef6625cea838e3e82def5c4b)) + + +### Bug Fixes + +* add settable poll delay for sse streams ([0392103](https://github.com/windmill-labs/windmill/commit/039210369383bcc3a15d95cba9efb591ee8e9891)) +* cli path on windows + error_handler_muted_on_cancel ([#6657](https://github.com/windmill-labs/windmill/issues/6657)) ([6ba3a43](https://github.com/windmill-labs/windmill/commit/6ba3a4397e439d40079d524de15507257442c5e1)) +* improve reliability of exits in case graceful handler didn't exit as expected ([f6dd78c](https://github.com/windmill-labs/windmill/commit/f6dd78cb11ee73408f66b4670c395ade99beedbe)) + ## [1.545.0](https://github.com/windmill-labs/windmill/compare/v1.544.2...v1.545.0) (2025-09-20) diff --git a/backend/.sqlx/query-07168aaf14cb6beff0ad4274b441f7f387f5055c47f493271d26731336257384.json b/backend/.sqlx/query-07168aaf14cb6beff0ad4274b441f7f387f5055c47f493271d26731336257384.json index e7ed0aee65..d29a18c691 100644 --- a/backend/.sqlx/query-07168aaf14cb6beff0ad4274b441f7f387f5055c47f493271d26731336257384.json +++ b/backend/.sqlx/query-07168aaf14cb6beff0ad4274b441f7f387f5055c47f493271d26731336257384.json @@ -46,11 +46,11 @@ ] }, "nullable": [ - false, - false, - false, - false, - false, + true, + true, + true, + true, + true, true, true ] diff --git a/backend/.sqlx/query-16e4b1bead9fc77fd98658b8cb8cc6d6bf1df758b30e99bd661da866062ef14f.json b/backend/.sqlx/query-16e4b1bead9fc77fd98658b8cb8cc6d6bf1df758b30e99bd661da866062ef14f.json new file mode 100644 index 0000000000..1af42ff529 --- /dev/null +++ b/backend/.sqlx/query-16e4b1bead9fc77fd98658b8cb8cc6d6bf1df758b30e99bd661da866062ef14f.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT hash FROM script WHERE path = $1 AND workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "hash", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "16e4b1bead9fc77fd98658b8cb8cc6d6bf1df758b30e99bd661da866062ef14f" +} diff --git a/backend/.sqlx/query-0fa105c49c8345916716514444bd3616ae4d114216c659233fbbc3c047e6b30a.json b/backend/.sqlx/query-5ad2c883d26f39f3c141806428a329951ef19a1cb3e1b429fcd1abe0e2db45b5.json similarity index 65% rename from backend/.sqlx/query-0fa105c49c8345916716514444bd3616ae4d114216c659233fbbc3c047e6b30a.json rename to backend/.sqlx/query-5ad2c883d26f39f3c141806428a329951ef19a1cb3e1b429fcd1abe0e2db45b5.json index c604c19f04..4b7a7ab231 100644 --- a/backend/.sqlx/query-0fa105c49c8345916716514444bd3616ae4d114216c659233fbbc3c047e6b30a.json +++ b/backend/.sqlx/query-5ad2c883d26f39f3c141806428a329951ef19a1cb3e1b429fcd1abe0e2db45b5.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n UPDATE\n workspace_settings\n SET\n error_handler = NULL,\n error_handler_extra_args = NULL,\n error_handler_muted_on_cancel = NULL\n WHERE\n workspace_id = $1\n ", + "query": "\n UPDATE\n workspace_settings\n SET\n error_handler = NULL,\n error_handler_extra_args = NULL,\n error_handler_muted_on_cancel = false\n WHERE\n workspace_id = $1\n ", "describe": { "columns": [], "parameters": { @@ -10,5 +10,5 @@ }, "nullable": [] }, - "hash": "0fa105c49c8345916716514444bd3616ae4d114216c659233fbbc3c047e6b30a" + "hash": "5ad2c883d26f39f3c141806428a329951ef19a1cb3e1b429fcd1abe0e2db45b5" } diff --git a/backend/.sqlx/query-804fc11e35f4afc0db194b6fe2594f91df7e588d4d2431bc85f4d8734920c8bf.json b/backend/.sqlx/query-804fc11e35f4afc0db194b6fe2594f91df7e588d4d2431bc85f4d8734920c8bf.json new file mode 100644 index 0000000000..6f08d98113 --- /dev/null +++ b/backend/.sqlx/query-804fc11e35f4afc0db194b6fe2594f91df7e588d4d2431bc85f4d8734920c8bf.json @@ -0,0 +1,51 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO script (summary, description, dedicated_worker, content, workspace_id, path, hash, language, tag, created_by, lock) VALUES ('', '', true, $1, $2, $3, $4, $5, $6, $7, '') ON CONFLICT (workspace_id, hash) DO NOTHING", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Varchar", + "Varchar", + "Int8", + { + "Custom": { + "name": "script_lang", + "kind": { + "Enum": [ + "python3", + "deno", + "go", + "bash", + "postgresql", + "nativets", + "bun", + "mysql", + "bigquery", + "snowflake", + "graphql", + "powershell", + "mssql", + "php", + "bunnative", + "rust", + "ansible", + "csharp", + "oracledb", + "nu", + "java", + "duckdb", + "ruby" + ] + } + } + }, + "Varchar", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "804fc11e35f4afc0db194b6fe2594f91df7e588d4d2431bc85f4d8734920c8bf" +} diff --git a/backend/.sqlx/query-85705fc3d7f8ba5f1b12d5fb222c38fc64deb1226aab9dc3bc4465324fce37d1.json b/backend/.sqlx/query-85705fc3d7f8ba5f1b12d5fb222c38fc64deb1226aab9dc3bc4465324fce37d1.json new file mode 100644 index 0000000000..bce7324fb6 --- /dev/null +++ b/backend/.sqlx/query-85705fc3d7f8ba5f1b12d5fb222c38fc64deb1226aab9dc3bc4465324fce37d1.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) SELECT unnest($1::uuid[]), $2, now(), $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "UuidArray", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "85705fc3d7f8ba5f1b12d5fb222c38fc64deb1226aab9dc3bc4465324fce37d1" +} diff --git a/backend/.sqlx/query-a0b3e10e077d30c1da135dff9feca3761d400391f1f46a8294da3e6c9af63887.json b/backend/.sqlx/query-a0b3e10e077d30c1da135dff9feca3761d400391f1f46a8294da3e6c9af63887.json new file mode 100644 index 0000000000..1afc61978e --- /dev/null +++ b/backend/.sqlx/query-a0b3e10e077d30c1da135dff9feca3761d400391f1f46a8294da3e6c9af63887.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO v2_job_status (id, flow_status) SELECT unnest($1::uuid[]), $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "UuidArray", + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "a0b3e10e077d30c1da135dff9feca3761d400391f1f46a8294da3e6c9af63887" +} diff --git a/backend/.sqlx/query-ab04cda71f8e2be9acbecabe1ee5ef756b8e5c1955fbe111df9ee171dc262338.json b/backend/.sqlx/query-ab04cda71f8e2be9acbecabe1ee5ef756b8e5c1955fbe111df9ee171dc262338.json new file mode 100644 index 0000000000..e07a3ccdd7 --- /dev/null +++ b/backend/.sqlx/query-ab04cda71f8e2be9acbecabe1ee5ef756b8e5c1955fbe111df9ee171dc262338.json @@ -0,0 +1,89 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO v2_job (id, runnable_id, runnable_path, kind, script_lang, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, raw_flow) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 FROM generate_series(1, 1)) RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Int8", + "Varchar", + { + "Custom": { + "name": "job_kind", + "kind": { + "Enum": [ + "script", + "preview", + "flow", + "dependencies", + "flowpreview", + "script_hub", + "identity", + "flowdependencies", + "http", + "graphql", + "postgresql", + "noop", + "appdependencies", + "deploymentcallback", + "singlescriptflow", + "flowscript", + "flownode", + "appscript", + "aiagent" + ] + } + } + }, + { + "Custom": { + "name": "script_lang", + "kind": { + "Enum": [ + "python3", + "deno", + "go", + "bash", + "postgresql", + "nativets", + "bun", + "mysql", + "bigquery", + "snowflake", + "graphql", + "powershell", + "mssql", + "php", + "bunnative", + "rust", + "ansible", + "csharp", + "oracledb", + "nu", + "java", + "duckdb", + "ruby" + ] + } + } + }, + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Jsonb" + ] + }, + "nullable": [ + false + ] + }, + "hash": "ab04cda71f8e2be9acbecabe1ee5ef756b8e5c1955fbe111df9ee171dc262338" +} diff --git a/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json b/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json index 54e94cfb8f..99269c9851 100644 --- a/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json +++ b/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json @@ -18,8 +18,8 @@ "Left": [] }, "nullable": [ - false, - true + true, + false ] }, "hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76" diff --git a/backend/.sqlx/query-b4a9abcb38997587b28655b0f4a212a5bd4039b57fab20b163617e33a4c9dd46.json b/backend/.sqlx/query-b4a9abcb38997587b28655b0f4a212a5bd4039b57fab20b163617e33a4c9dd46.json new file mode 100644 index 0000000000..a49baeefaf --- /dev/null +++ b/backend/.sqlx/query-b4a9abcb38997587b28655b0f4a212a5bd4039b57fab20b163617e33a4c9dd46.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "UuidArray" + ] + }, + "nullable": [] + }, + "hash": "b4a9abcb38997587b28655b0f4a212a5bd4039b57fab20b163617e33a4c9dd46" +} diff --git a/backend/.sqlx/query-ff0403790674cdb07022af71c2377afbd8b3a660b3be27514b517c077c63c238.json b/backend/.sqlx/query-ff0403790674cdb07022af71c2377afbd8b3a660b3be27514b517c077c63c238.json new file mode 100644 index 0000000000..0e42bd0fdb --- /dev/null +++ b/backend/.sqlx/query-ff0403790674cdb07022af71c2377afbd8b3a660b3be27514b517c077c63c238.json @@ -0,0 +1,89 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO v2_job (id, runnable_id, runnable_path, kind, script_lang, tag, created_by, permissioned_as, permissioned_as_email, workspace_id) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9 FROM generate_series(1, $10)) RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Int8", + "Varchar", + { + "Custom": { + "name": "job_kind", + "kind": { + "Enum": [ + "script", + "preview", + "flow", + "dependencies", + "flowpreview", + "script_hub", + "identity", + "flowdependencies", + "http", + "graphql", + "postgresql", + "noop", + "appdependencies", + "deploymentcallback", + "singlescriptflow", + "flowscript", + "flownode", + "appscript", + "aiagent" + ] + } + } + }, + { + "Custom": { + "name": "script_lang", + "kind": { + "Enum": [ + "python3", + "deno", + "go", + "bash", + "postgresql", + "nativets", + "bun", + "mysql", + "bigquery", + "snowflake", + "graphql", + "powershell", + "mssql", + "php", + "bunnative", + "rust", + "ansible", + "csharp", + "oracledb", + "nu", + "java", + "duckdb", + "ruby" + ] + } + } + }, + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Int4" + ] + }, + "nullable": [ + false + ] + }, + "hash": "ff0403790674cdb07022af71c2377afbd8b3a660b3be27514b517c077c63c238" +} diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 26c0de648f..a961a10320 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -2027,7 +2027,7 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading 0.8.8", + "libloading 0.8.9", ] [[package]] @@ -2508,7 +2508,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b28bfe653d79bd16c77f659305b195b82bb5ce0c0eb2a4846b82ddbd77586813" dependencies = [ "bitflags 2.9.4", - "libloading 0.8.8", + "libloading 0.8.9", "winapi", ] @@ -7318,7 +7318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" dependencies = [ "libc", - "libloading 0.8.8", + "libloading 0.8.9", "pkg-config", ] @@ -7525,9 +7525,9 @@ checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" [[package]] name = "lexical-core" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b765c31809609075565a70b4b71402281283aeda7ecaf4818ac14a7b2ade8958" +checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594" dependencies = [ "lexical-parse-float", "lexical-parse-integer", @@ -7538,53 +7538,46 @@ dependencies = [ [[package]] name = "lexical-parse-float" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de6f9cb01fb0b08060209a057c048fcbab8717b4c1ecd2eac66ebfe39a65b0f2" +checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" dependencies = [ "lexical-parse-integer", "lexical-util", - "static_assertions", ] [[package]] name = "lexical-parse-integer" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72207aae22fc0a121ba7b6d479e42cbfea549af1479c3f3a4f12c70dd66df12e" +checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" dependencies = [ "lexical-util", - "static_assertions", ] [[package]] name = "lexical-util" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a82e24bf537fd24c177ffbbdc6ebcc8d54732c35b50a3f28cc3f4e4c949a0b3" -dependencies = [ - "static_assertions", -] +checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" [[package]] name = "lexical-write-float" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5afc668a27f460fb45a81a757b6bf2f43c2d7e30cb5a2dcd3abf294c78d62bd" +checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361" dependencies = [ "lexical-util", "lexical-write-integer", - "static_assertions", ] [[package]] name = "lexical-write-integer" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "629ddff1a914a836fb245616a7888b62903aae58fa771e1d83943035efa0f978" +checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df" dependencies = [ "lexical-util", - "static_assertions", ] [[package]] @@ -7624,12 +7617,12 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.53.3", + "windows-link 0.2.0", ] [[package]] @@ -10863,9 +10856,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "0.2.1" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37f2048a81a7ff7e8ef6bc5abced70c3d9114c8f03d85d7aaaafd9fd04f12e9e" +checksum = "41ab0892f4938752b34ae47cb53910b1b0921e55e77ddb6e44df666cab17939f" dependencies = [ "base64 0.22.1", "bytes", @@ -10878,7 +10871,7 @@ dependencies = [ "pin-project-lite", "rand 0.9.0", "rmcp-macros", - "schemars 0.8.22", + "schemars 1.0.4", "serde", "serde_json", "sse-stream", @@ -10893,11 +10886,11 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "0.2.1" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72398e694b9f6dbb5de960cf158c8699e6a1854cb5bbaac7de0646b2005763c4" +checksum = "1827cd98dab34cade0513243c6fe0351f0f0b2c9d6825460bcf45b42804bdda0" dependencies = [ - "darling 0.20.11", + "darling 0.21.3", "proc-macro2", "quote", "serde_json", @@ -11179,7 +11172,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.4.0", + "security-framework 3.5.0", ] [[package]] @@ -11438,9 +11431,8 @@ version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" dependencies = [ - "chrono", "dyn-clone", - "schemars_derive", + "schemars_derive 0.8.22", "serde", "serde_json", ] @@ -11463,8 +11455,10 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" dependencies = [ + "chrono", "dyn-clone", "ref-cast", + "schemars_derive 1.0.4", "serde", "serde_json", ] @@ -11481,6 +11475,18 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "schemars_derive" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.106", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -11560,9 +11566,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b369d18893388b345804dc0007963c99b7d665ae71d275812d828c6f089640" +checksum = "cc198e42d9b7510827939c9a15f5062a0c913f3371d765977e586d2fe6c16f4a" dependencies = [ "bitflags 2.9.4", "core-foundation 0.10.1", @@ -13298,9 +13304,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.22.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", "getrandom 0.3.3", @@ -14330,7 +14336,7 @@ checksum = "90b70b37e9074642bc5f60bb23247fd072a84314ca9e71cdf8527593406a0dd3" dependencies = [ "gemm 0.18.2", "half", - "libloading 0.8.8", + "libloading 0.8.9", "memmap2 0.9.8", "num", "num-traits", @@ -15023,7 +15029,7 @@ dependencies = [ "js-sys", "khronos-egl", "libc", - "libloading 0.8.8", + "libloading 0.8.9", "log", "metal", "naga", @@ -15129,7 +15135,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windmill" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "aws-sdk-config", @@ -15148,7 +15154,7 @@ dependencies = [ "k8s-openapi", "kube", "lazy_static", - "libloading 0.8.8", + "libloading 0.8.9", "memchr", "nom 8.0.0", "object_store", @@ -15189,7 +15195,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "argon2", @@ -15309,7 +15315,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.545.0" +version = "1.546.1" dependencies = [ "base64 0.22.1", "chrono", @@ -15324,7 +15330,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.545.0" +version = "1.546.1" dependencies = [ "chrono", "serde", @@ -15337,7 +15343,7 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "axum", @@ -15356,7 +15362,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "async-recursion", @@ -15441,7 +15447,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.545.0" +version = "1.546.1" dependencies = [ "regex", "serde", @@ -15456,7 +15462,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "bytes", @@ -15480,7 +15486,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.545.0" +version = "1.546.1" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -15492,7 +15498,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.545.0" +version = "1.546.1" dependencies = [ "convert_case 0.6.0", "serde", @@ -15501,7 +15507,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "lazy_static", @@ -15513,7 +15519,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "serde_json", @@ -15525,7 +15531,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "gosyn", @@ -15537,7 +15543,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "lazy_static", @@ -15549,7 +15555,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "serde_json", @@ -15561,7 +15567,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "nu-parser", @@ -15572,7 +15578,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "itertools 0.14.0", @@ -15583,7 +15589,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "itertools 0.14.0", @@ -15595,7 +15601,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "async-recursion", @@ -15618,7 +15624,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "lazy_static", @@ -15632,7 +15638,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -15649,7 +15655,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "lazy_static", @@ -15663,7 +15669,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "lazy_static", @@ -15681,7 +15687,7 @@ dependencies = [ [[package]] name = "windmill-parser-wasm" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "getrandom 0.2.16", @@ -15706,7 +15712,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "serde_json", @@ -15716,7 +15722,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "async-recursion", @@ -15749,7 +15755,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.545.0" +version = "1.546.1" dependencies = [ "wasm-bindgen", "wasm-bindgen-test", @@ -15759,7 +15765,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.545.0" +version = "1.546.1" dependencies = [ "anyhow", "async-once-cell", @@ -15799,7 +15805,7 @@ dependencies = [ "jsonwebtoken 8.3.0", "lazy_static", "libffi-sys", - "libloading 0.8.8", + "libloading 0.8.9", "mappable-rc", "mime_guess", "mysql_async", @@ -16446,9 +16452,9 @@ dependencies = [ [[package]] name = "xattr" -version = "1.5.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", "rustix 1.1.2", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 8b6d9b4b60..4e8811a52c 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.545.0" +version = "1.546.1" authors.workspace = true edition.workspace = true @@ -34,7 +34,7 @@ members = [ exclude = ["./windmill-duckdb-ffi-internal"] [workspace.package] -version = "1.545.0" +version = "1.546.1" authors = ["Ruben Fiszel "] edition = "2021" diff --git a/backend/windmill-api/Cargo.toml b/backend/windmill-api/Cargo.toml index 60d79e3b31..76d05a7188 100644 --- a/backend/windmill-api/Cargo.toml +++ b/backend/windmill-api/Cargo.toml @@ -40,7 +40,7 @@ mcp = ["dep:rmcp"] python = [] [dependencies] -rmcp = { version = "0.2.1", features=["transport-streamable-http-server", "transport-streamable-http-server-session", "transport-worker"], optional = true } +rmcp = { version = "0.6.4", features=["transport-streamable-http-server", "transport-streamable-http-server-session", "transport-worker"], optional = true } windmill-queue.workspace = true windmill-common = { workspace = true, default-features = false } windmill-audit.workspace = true diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index deed7cb205..3c2d957947 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.545.0 + version: 1.546.1 title: Windmill API contact: diff --git a/backend/windmill-api/src/folders.rs b/backend/windmill-api/src/folders.rs index f24026c580..9cf3c9af2b 100644 --- a/backend/windmill-api/src/folders.rs +++ b/backend/windmill-api/src/folders.rs @@ -361,6 +361,11 @@ async fn update_folder( ); } if let Some(extra_perms) = ng.extra_perms { + if !extra_perms.is_object() { + return Err(windmill_common::error::Error::BadRequest(format!( + "extra_perms must be an object, received {}", extra_perms.to_string() + ))); + } sqlb.set( "extra_perms", "?".bind(&serde_json::to_string(&extra_perms).map_err(to_anyhow)?), diff --git a/backend/windmill-api/src/mcp/server.rs b/backend/windmill-api/src/mcp/server.rs index e51d008e3c..fa8644b5b8 100644 --- a/backend/windmill-api/src/mcp/server.rs +++ b/backend/windmill-api/src/mcp/server.rs @@ -4,16 +4,17 @@ //! specification. This is a thin orchestration layer that delegates to the appropriate //! modules for tool management, database operations, and schema transformation. -use std::borrow::Cow; use std::collections::HashMap; use std::sync::Arc; +use std::{borrow::Cow, time::Duration}; use axum::body::to_bytes; use rmcp::{ handler::server::ServerHandler, model::*, service::{RequestContext, RoleServer}, - Error, + transport::StreamableHttpServerConfig, + ErrorData, }; use serde_json::Value; use tokio::try_join; @@ -26,35 +27,29 @@ use crate::jobs::{ run_wait_result_flow_by_path_internal, run_wait_result_script_by_path_internal, RunJobQuery, }; +use super::tools::endpoint_tools::{ + all_endpoint_tools, call_endpoint_tool, endpoint_tools_to_mcp_tools, EndpointTool, +}; use super::utils::{ database::{ - check_scopes, get_items, get_resources_types, get_scripts_from_hub, get_item_schema, get_hub_script_schema + check_scopes, get_hub_script_schema, get_item_schema, get_items, get_resources_types, + get_scripts_from_hub, + }, + models::{ + FlowInfo, ResourceInfo, ResourceType, SchemaType, ScriptInfo, ToolableItem, WorkspaceId, }, - models::{ScriptInfo, FlowInfo, ResourceInfo, ResourceType, SchemaType, ToolableItem, WorkspaceId}, schema::transform_schema_for_resources, transform::{reverse_transform, reverse_transform_key}, }; -use super::tools::{ - endpoint_tools::{all_endpoint_tools, endpoint_tools_to_mcp_tools, call_endpoint_tool, EndpointTool}, -}; use axum::{ - extract::Path, - http::Request, - middleware::Next, - response::Response, - routing::get, - Json, - Router, + extract::Path, http::Request, middleware::Next, response::Response, routing::get, Json, Router, }; use rmcp::transport::streamable_http_server::{ - session::local::LocalSessionManager, - SessionManager, - StreamableHttpService, + session::local::LocalSessionManager, SessionManager, StreamableHttpService, }; use windmill_common::error::JsonResult; - /// MCP Server Runner - implements the core MCP protocol handlers #[derive(Clone)] pub struct Runner {} @@ -72,7 +67,7 @@ impl Runner { workspace_id: &str, resources_cache: &mut HashMap>, resources_types: &Vec, - ) -> Result { + ) -> Result { let is_hub = item.is_hub(); let path = item.get_path_or_id(); let item_type = item.item_type(); @@ -120,52 +115,57 @@ impl Runner { name: Cow::Owned(path), description: Some(Cow::Owned(description)), input_schema: Arc::new(input_schema_map), + title: Some(item.get_summary().to_string()), + output_schema: None, + icons: None, annotations: Some(ToolAnnotations { title: Some(item.get_summary().to_string()), - read_only_hint: Some(false), // Can modify environment + 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 - open_world_hint: Some(true), // Can interact with external services + open_world_hint: Some(true), // Can interact with external services }), }) } } - impl ServerHandler for Runner { /// Handles the `CallTool` request from the MCP client async fn call_tool( &self, request: CallToolRequestParam, context: RequestContext, - ) -> Result { + ) -> Result { let http_parts = context .extensions .get::() .ok_or_else(|| { tracing::error!("http::request::Parts not found"); - Error::internal_error("http::request::Parts not found", None) + ErrorData::internal_error("http::request::Parts not found", None) })?; let authed = http_parts.extensions.get::().ok_or_else(|| { tracing::error!("ApiAuthed Axum extension not found"); - Error::internal_error("ApiAuthed Axum extension not found", None) + ErrorData::internal_error("ApiAuthed Axum extension not found", None) })?; check_scopes(authed)?; let db = http_parts.extensions.get::().ok_or_else(|| { tracing::error!("DB Axum extension not found"); - Error::internal_error("DB Axum extension not found", None) + ErrorData::internal_error("DB Axum extension not found", None) })?; let user_db = http_parts.extensions.get::().ok_or_else(|| { tracing::error!("UserDB Axum extension not found"); - Error::internal_error("UserDB Axum extension not found", None) + ErrorData::internal_error("UserDB Axum extension not found", None) })?; - + let args = request.arguments.map(Value::Object).ok_or_else(|| { - Error::invalid_params("Missing arguments for tool", Some(request.name.clone().into())) + ErrorData::invalid_params( + "Missing arguments for tool", + Some(request.name.clone().into()), + ) })?; let workspace_id = http_parts @@ -173,7 +173,7 @@ impl ServerHandler for Runner { .get::() .ok_or_else(|| { tracing::error!("WorkspaceId not found"); - Error::internal_error("WorkspaceId not found", None) + ErrorData::internal_error("WorkspaceId not found", None) }) .map(|w_id| w_id.0.clone())?; @@ -182,18 +182,20 @@ impl ServerHandler for Runner { for endpoint_tool in endpoint_tools { if endpoint_tool.name.as_ref() == request.name { // This is an endpoint tool, forward to the actual HTTP endpoint - let result = call_endpoint_tool(&endpoint_tool, args.clone(), &workspace_id, &authed).await?; + let result = + call_endpoint_tool(&endpoint_tool, args.clone(), &workspace_id, &authed) + .await?; return Ok(CallToolResult::success(vec![Content::text( - serde_json::to_string_pretty(&result).unwrap_or_else(|_| "{}".to_string()) + serde_json::to_string_pretty(&result).unwrap_or_else(|_| "{}".to_string()), )])); } } // Continue with script/flow logic let (tool_type, path, is_hub) = reverse_transform(&request.name).map_err(|e| { - Error::internal_error(format!("Failed to reverse transform path: {}", e), None) + ErrorData::internal_error(format!("Failed to reverse transform path: {}", e), None) })?; - + let item_schema = if is_hub { get_hub_script_schema(&format!("hub/{}", path), db).await? } else { @@ -259,14 +261,20 @@ impl ServerHandler for Runner { let body_bytes = to_bytes(response.into_body(), usize::MAX) .await .map_err(|e| { - Error::internal_error(format!("Failed to read response body: {}", e), None) + ErrorData::internal_error( + format!("Failed to read response body: {}", e), + None, + ) })?; let body_str = String::from_utf8(body_bytes.to_vec()).map_err(|e| { - Error::internal_error(format!("Failed to decode response body: {}", e), None) + ErrorData::internal_error( + format!("Failed to decode response body: {}", e), + None, + ) })?; Ok(CallToolResult::success(vec![Content::text(body_str)])) } - Err(e) => Err(Error::internal_error( + Err(e) => Err(ErrorData::internal_error( format!("Failed to run script: {}", e), None, )), @@ -278,30 +286,30 @@ impl ServerHandler for Runner { &self, _request: Option, mut _context: RequestContext, - ) -> Result { + ) -> Result { let http_parts = _context .extensions .get::() .ok_or_else(|| { tracing::error!("http::request::Parts not found"); - Error::internal_error("http::request::Parts not found", None) + ErrorData::internal_error("http::request::Parts not found", None) })?; let authed = http_parts.extensions.get::().ok_or_else(|| { tracing::error!("ApiAuthed Axum extension not found"); - Error::internal_error("ApiAuthed Axum extension not found", None) + ErrorData::internal_error("ApiAuthed Axum extension not found", None) })?; check_scopes(authed)?; let db = http_parts.extensions.get::().ok_or_else(|| { tracing::error!("DB Axum extension not found"); - Error::internal_error("DB Axum extension not found", None) + ErrorData::internal_error("DB Axum extension not found", None) })?; let user_db = http_parts.extensions.get::().ok_or_else(|| { tracing::error!("UserDB Axum extension not found"); - Error::internal_error("UserDB Axum extension not found", None) + ErrorData::internal_error("UserDB Axum extension not found", None) })?; let workspace_id = http_parts @@ -309,7 +317,7 @@ impl ServerHandler for Runner { .get::() .ok_or_else(|| { tracing::error!("WorkspaceId not found"); - Error::internal_error("WorkspaceId not found", None) + ErrorData::internal_error("WorkspaceId not found", None) }) .map(|w_id| w_id.0.clone())?; @@ -319,10 +327,18 @@ impl ServerHandler for Runner { .iter() .find(|scope| scope.starts_with("mcp:") && !scope.contains("hub")) }); - let hub_scope = scopes.and_then(|scopes| scopes.iter().find(|scope| scope.starts_with("mcp:hub"))); + let hub_scope = + scopes.and_then(|scopes| scopes.iter().find(|scope| scope.starts_with("mcp:hub"))); let (scope_type, scope_path) = owned_scope.map_or(("all", None), |scope| { let parts = scope.split(":").collect::>(); - (parts[1], if parts.len() == 3 { Some(parts[2]) } else { None }) + ( + parts[1], + if parts.len() == 3 { + Some(parts[2]) + } else { + None + }, + ) }); let scope_integrations = hub_scope.and_then(|scope| { let parts = scope.split(":").collect::>(); @@ -341,8 +357,14 @@ impl ServerHandler for Runner { "script", scope_path.as_deref(), ); - let flows_fn = - get_items::(user_db, authed, &workspace_id, scope_type, "flow", scope_path.as_deref()); + let flows_fn = get_items::( + user_db, + authed, + &workspace_id, + scope_type, + "flow", + scope_path.as_deref(), + ); let resources_types_fn = get_resources_types(user_db, authed, &workspace_id); let hub_scripts_fn = get_scripts_from_hub(db, scope_integrations.as_deref()); let (scripts, flows, resources_types, hub_scripts) = if scope_integrations.is_some() { @@ -410,7 +432,7 @@ impl ServerHandler for Runner { fn get_info(&self) -> ServerInfo { ServerInfo { - protocol_version: Default::default(), + protocol_version: ProtocolVersion::default(), capabilities: ServerCapabilities::builder() .enable_tools() .enable_tool_list_changed() @@ -424,7 +446,7 @@ impl ServerHandler for Runner { &self, _request: InitializeRequestParam, _context: RequestContext, - ) -> Result { + ) -> Result { Ok(self.get_info()) } @@ -432,7 +454,7 @@ impl ServerHandler for Runner { &self, _request: Option, _context: RequestContext, - ) -> Result { + ) -> Result { Ok(ListResourcesResult { resources: vec![], next_cursor: None }) } @@ -440,7 +462,7 @@ impl ServerHandler for Runner { &self, _request: Option, _context: RequestContext, - ) -> Result { + ) -> Result { Ok(ListPromptsResult::default()) } @@ -448,7 +470,7 @@ impl ServerHandler for Runner { &self, _request: Option, _context: RequestContext, - ) -> Result { + ) -> Result { Ok(ListResourceTemplatesResult::default()) } } @@ -467,7 +489,10 @@ pub async fn extract_and_store_workspace_id( /// Setup the MCP server with HTTP transport pub async fn setup_mcp_server() -> anyhow::Result<(Router, Arc)> { let session_manager = Arc::new(LocalSessionManager::default()); - let service_config = Default::default(); + let service_config = StreamableHttpServerConfig { + sse_keep_alive: Some(Duration::from_secs(15)), + stateful_mode: false, + }; let service = StreamableHttpService::new( || Ok(Runner::new()), session_manager.clone(), @@ -513,6 +538,5 @@ async fn list_mcp_tools_handler() -> JsonResult> { /// Creates a router service for listing MCP tools pub fn list_tools_service() -> Router { - Router::new() - .route("/", get(list_mcp_tools_handler)) -} \ No newline at end of file + Router::new().route("/", get(list_mcp_tools_handler)) +} diff --git a/backend/windmill-api/src/mcp/tools/endpoint_tools.rs b/backend/windmill-api/src/mcp/tools/endpoint_tools.rs index 1b8a7f0a66..624ca29c7b 100644 --- a/backend/windmill-api/src/mcp/tools/endpoint_tools.rs +++ b/backend/windmill-api/src/mcp/tools/endpoint_tools.rs @@ -3,16 +3,16 @@ //! Contains the auto-generated endpoint tools and utilities for converting //! them to MCP tools and handling HTTP calls to Windmill API endpoints. -use rmcp::{model::Tool, Error}; +use crate::db::ApiAuthed; +use rmcp::{model::Tool, ErrorData}; use std::sync::Arc; use windmill_common::auth::create_jwt_token; use windmill_common::db::Authed; use windmill_common::BASE_URL; -use crate::db::ApiAuthed; // Import the auto-generated tools use super::auto_generated_endpoints; -pub use auto_generated_endpoints::{EndpointTool, all_tools}; +pub use auto_generated_endpoints::{all_tools, EndpointTool}; /// Get all available endpoint tools pub fn all_endpoint_tools() -> Vec { @@ -21,25 +21,28 @@ pub fn all_endpoint_tools() -> Vec { /// Convert endpoint tools to MCP tools pub fn endpoint_tools_to_mcp_tools(endpoint_tools: Vec) -> Vec { - endpoint_tools.into_iter().map(|tool| endpoint_tool_to_mcp_tool(&tool)).collect() + endpoint_tools + .into_iter() + .map(|tool| endpoint_tool_to_mcp_tool(&tool)) + .collect() } /// Convert a single endpoint tool to MCP tool pub fn endpoint_tool_to_mcp_tool(tool: &EndpointTool) -> Tool { let mut combined_properties = serde_json::Map::new(); let mut combined_required = Vec::new(); - + // Combine all parameter schemas let schemas = [ &tool.path_params_schema, - &tool.query_params_schema, + &tool.query_params_schema, &tool.body_schema, ]; - + for schema in schemas.iter().filter_map(|s| s.as_ref()) { merge_schema_into(&mut combined_properties, &mut combined_required, schema); } - + let combined_schema = serde_json::json!({ "type": "object", "properties": combined_properties, @@ -47,7 +50,7 @@ pub fn endpoint_tool_to_mcp_tool(tool: &EndpointTool) -> Tool { }); let description = format!("{}. {}", tool.description, tool.instructions); - + // Create annotations based on HTTP method and endpoint characteristics let annotations = create_endpoint_annotations(tool); @@ -55,6 +58,9 @@ pub fn endpoint_tool_to_mcp_tool(tool: &EndpointTool) -> Tool { name: tool.name.clone(), description: Some(description.into()), input_schema: Arc::new(combined_schema.as_object().unwrap().clone()), + title: Some(tool.name.to_string()), + output_schema: None, + icons: None, annotations: Some(annotations), } } @@ -62,15 +68,15 @@ pub fn endpoint_tool_to_mcp_tool(tool: &EndpointTool) -> Tool { /// Create appropriate annotations for endpoint tools based on HTTP method fn create_endpoint_annotations(tool: &EndpointTool) -> rmcp::model::ToolAnnotations { let method = tool.method.as_ref(); - + // Determine characteristics based on HTTP method let (read_only, destructive, idempotent, open_world) = match method { - "GET" => (true, false, true, true), // Read-only, safe, idempotent - "POST" => (false, true, false, true), // Can modify, potentially destructive, not idempotent - "PUT" => (false, false, true, true), // Can modify, typically idempotent updates - "DELETE" => (false, true, true, true), // Destructive but idempotent + "GET" => (true, false, true, true), // Read-only, safe, idempotent + "POST" => (false, true, false, true), // Can modify, potentially destructive, not idempotent + "PUT" => (false, false, true, true), // Can modify, typically idempotent updates + "DELETE" => (false, true, true, true), // Destructive but idempotent "PATCH" => (false, false, false, true), // Partial updates, not guaranteed idempotent - _ => (false, true, false, true), // Default: assume can modify and be destructive + _ => (false, true, false, true), // Default: assume can modify and be destructive }; rmcp::model::ToolAnnotations { @@ -93,7 +99,7 @@ fn merge_schema_into( combined_properties.insert(key.clone(), value.clone()); } } - + if let Some(required) = schema.get("required").and_then(|r| r.as_array()) { for req in required.iter().filter_map(|r| r.as_str()) { combined_required.push(req.to_string()); @@ -107,34 +113,52 @@ pub async fn call_endpoint_tool( args: serde_json::Value, workspace_id: &str, api_authed: &ApiAuthed, -) -> Result { +) -> Result { let args_map = match &args { serde_json::Value::Object(map) => map, - _ => return Err(Error::invalid_params("Arguments must be an object", Some(tool.name.clone().into()))), + _ => { + return Err(ErrorData::invalid_params( + "Arguments must be an object", + Some(tool.name.clone().into()), + )) + } }; // Build URL with path substitutions - let path_template = substitute_path_params(&tool.path, workspace_id, args_map, &tool.path_params_schema)?; + let path_template = + substitute_path_params(&tool.path, workspace_id, args_map, &tool.path_params_schema)?; let query_string = build_query_string(args_map, &tool.query_params_schema); - let full_url = format!("{}/api{}{}", BASE_URL.read().await, path_template, query_string); + let full_url = format!( + "{}/api{}{}", + BASE_URL.read().await, + path_template, + query_string + ); // Prepare request body let body_json = build_request_body(&tool.method, args_map, &tool.body_schema); // Create and execute request - let response = create_http_request(&tool.method, &full_url, workspace_id, api_authed, body_json).await?; - + let response = + create_http_request(&tool.method, &full_url, workspace_id, api_authed, body_json).await?; + let status = response.status(); let response_text = response.text().await.map_err(|e| { - Error::internal_error(format!("Failed to read response text: {}", e), None) + ErrorData::internal_error(format!("Failed to read response text: {}", e), None) })?; if status.is_success() { - Ok(serde_json::from_str(&response_text).unwrap_or_else(|_| serde_json::Value::String(response_text))) + Ok(serde_json::from_str(&response_text) + .unwrap_or_else(|_| serde_json::Value::String(response_text))) } else { - Err(Error::internal_error( - format!("HTTP {} {}: {}", status.as_u16(), status.canonical_reason().unwrap_or(""), response_text), - None + Err(ErrorData::internal_error( + format!( + "HTTP {} {}: {}", + status.as_u16(), + status.canonical_reason().unwrap_or(""), + response_text + ), + None, )) } } @@ -145,9 +169,9 @@ fn substitute_path_params( workspace_id: &str, args_map: &serde_json::Map, path_schema: &Option, -) -> Result { +) -> Result { let mut path_template = path.replace("{workspace}", workspace_id); - + if let Some(schema) = path_schema { if let Some(props) = schema.get("properties").and_then(|p| p.as_object()) { for (param_name, _) in props { @@ -157,19 +181,19 @@ fn substitute_path_params( if let Some(str_val) = param_value.as_str() { path_template = path_template.replace(&placeholder, str_val); } - }, + } None => { tracing::warn!("Missing required path parameter: {}", param_name); - return Err(Error::invalid_params( + return Err(ErrorData::invalid_params( format!("Missing required path parameter: {}", param_name), - None + None, )); } } } } } - + Ok(path_template) } @@ -178,25 +202,31 @@ fn build_query_string( args_map: &serde_json::Map, query_schema: &Option, ) -> String { - let Some(schema) = query_schema else { return String::new() }; - let Some(props) = schema.get("properties").and_then(|p| p.as_object()) else { return String::new() }; - + let Some(schema) = query_schema else { + return String::new(); + }; + let Some(props) = schema.get("properties").and_then(|p| p.as_object()) else { + return String::new(); + }; + let query_params: Vec = props .keys() .filter_map(|param_name| { - args_map.get(param_name) + args_map + .get(param_name) .filter(|v| !v.is_null()) .map(|value| { let value_str = value.to_string(); let str_val = value_str.trim_matches('"'); - format!("{}={}", - urlencoding::encode(param_name), + format!( + "{}={}", + urlencoding::encode(param_name), urlencoding::encode(str_val) ) }) }) .collect(); - + if query_params.is_empty() { String::new() } else { @@ -213,18 +243,19 @@ fn build_request_body( if method == "GET" { return None; } - + let schema = body_schema.as_ref()?; let props = schema.get("properties")?.as_object()?; - + let body_map: serde_json::Map = props .keys() .filter_map(|param_name| { - args_map.get(param_name) + args_map + .get(param_name) .map(|value| (param_name.clone(), value.clone())) }) .collect(); - + if body_map.is_empty() { None } else { @@ -239,7 +270,7 @@ async fn create_http_request( workspace_id: &str, api_authed: &ApiAuthed, body_json: Option, -) -> Result { +) -> Result { let client = &crate::HTTP_CLIENT; let mut request_builder = match method { "GET" => client.get(url), @@ -247,16 +278,19 @@ async fn create_http_request( "PUT" => client.put(url), "DELETE" => client.delete(url), "PATCH" => client.patch(url), - _ => return Err(Error::invalid_params( - format!("Unsupported HTTP method: {}", method), - None - )), + _ => { + return Err(ErrorData::invalid_params( + format!("Unsupported HTTP method: {}", method), + None, + )) + } }; // Add authorization header let authed = Authed::from(api_authed.clone()); - let token = create_jwt_token(authed, workspace_id, 3600, None, None, None, None).await - .map_err(|e| Error::internal_error(e.to_string(), None))?; + let token = create_jwt_token(authed, workspace_id, 3600, None, None, None, None) + .await + .map_err(|e| ErrorData::internal_error(e.to_string(), None))?; request_builder = request_builder.header("Authorization", format!("Bearer {}", token)); // Add body if present @@ -266,7 +300,8 @@ async fn create_http_request( .json(&body); } - request_builder.send().await.map_err(|e| { - Error::internal_error(format!("Failed to execute request: {}", e), None) - }) -} \ No newline at end of file + request_builder + .send() + .await + .map_err(|e| ErrorData::internal_error(format!("Failed to execute request: {}", e), None)) +} diff --git a/backend/windmill-api/src/mcp/utils/database.rs b/backend/windmill-api/src/mcp/utils/database.rs index 0a0ea15ca6..39aac1ae6e 100644 --- a/backend/windmill-api/src/mcp/utils/database.rs +++ b/backend/windmill-api/src/mcp/utils/database.rs @@ -3,28 +3,32 @@ //! Contains all database query functions and database-related utilities //! used by the MCP server implementation. -use rmcp::Error; +use rmcp::ErrorData; use sql_builder::prelude::*; use windmill_common::db::UserDB; use windmill_common::scripts::{get_full_hub_script_by_path, Schema}; use windmill_common::utils::{query_elems_from_hub, StripPath}; use windmill_common::{DB, HUB_BASE_URL}; +use super::models::*; use crate::db::ApiAuthed; use crate::HTTP_CLIENT; -use super::models::*; /// Check if the user has proper MCP scopes -pub fn check_scopes(authed: &ApiAuthed) -> Result<(), Error> { +pub fn check_scopes(authed: &ApiAuthed) -> Result<(), ErrorData> { let scopes = authed.scopes.as_ref(); if scopes.is_none() - || scopes - .unwrap() - .iter() - .all(|scope| !scope.starts_with("mcp:all") && !scope.starts_with("mcp:favorites") && !scope.starts_with("mcp:hub:")) + || scopes.unwrap().iter().all(|scope| { + !scope.starts_with("mcp:all") + && !scope.starts_with("mcp:favorites") + && !scope.starts_with("mcp:hub:") + }) { tracing::error!("Unauthorized: missing mcp scope"); - return Err(Error::internal_error("Unauthorized: missing mcp scope".to_string(), None)); + return Err(ErrorData::internal_error( + "Unauthorized: missing mcp scope".to_string(), + None, + )); } Ok(()) } @@ -36,7 +40,7 @@ pub async fn get_item_schema( authed: &ApiAuthed, workspace_id: &str, item_type: &str, -) -> Result, Error> { +) -> Result, ErrorData> { let mut sqlb = SqlBuilder::select_from(&format!("{} as o", item_type)); sqlb.fields(&["o.schema"]); sqlb.and_where("o.path = ?".bind(&path)); @@ -45,23 +49,23 @@ pub async fn get_item_schema( sqlb.and_where("o.draft_only IS NOT TRUE"); let sql = sqlb.sql().map_err(|_e| { tracing::error!("failed to build sql: {}", _e); - Error::internal_error("failed to build sql", None) + ErrorData::internal_error("failed to build sql", None) })?; let mut tx = user_db .clone() .begin(authed) .await - .map_err(|_e| Error::internal_error("failed to begin transaction", None))?; + .map_err(|_e| ErrorData::internal_error("failed to begin transaction", None))?; let item = sqlx::query_as::<_, ItemSchema>(&sql) .fetch_one(&mut *tx) .await .map_err(|_e| { tracing::error!("failed to fetch item schema: {}", _e); - Error::internal_error("failed to fetch item schema", None) + ErrorData::internal_error("failed to fetch item schema", None) })?; tx.commit() .await - .map_err(|_e| Error::internal_error("failed to commit transaction", None))?; + .map_err(|_e| ErrorData::internal_error("failed to commit transaction", None))?; Ok(item.schema) } @@ -70,29 +74,29 @@ pub async fn get_resources_types( user_db: &UserDB, authed: &ApiAuthed, workspace_id: &str, -) -> Result, Error> { +) -> Result, ErrorData> { let mut sqlb = SqlBuilder::select_from("resource_type as o"); sqlb.fields(&["o.name", "o.description"]); sqlb.and_where("o.workspace_id = ?".bind(&workspace_id)); let sql = sqlb.sql().map_err(|_e| { tracing::error!("failed to build sql: {}", _e); - Error::internal_error("failed to build sql", None) + ErrorData::internal_error("failed to build sql", None) })?; let mut tx = user_db .clone() .begin(authed) .await - .map_err(|_e| Error::internal_error("failed to begin transaction", None))?; + .map_err(|_e| ErrorData::internal_error("failed to begin transaction", None))?; let rows = sqlx::query_as::<_, ResourceType>(&sql) .fetch_all(&mut *tx) .await .map_err(|_e| { tracing::error!("Failed to fetch resource types: {}", _e); - Error::internal_error("failed to fetch resource types", None) + ErrorData::internal_error("failed to fetch resource types", None) })?; tx.commit() .await - .map_err(|_e| Error::internal_error("failed to commit transaction", None))?; + .map_err(|_e| ErrorData::internal_error("failed to commit transaction", None))?; Ok(rows) } @@ -102,30 +106,30 @@ pub async fn get_resources( authed: &ApiAuthed, workspace_id: &str, resource_type: &str, -) -> Result, Error> { +) -> Result, ErrorData> { let mut sqlb = SqlBuilder::select_from("resource as o"); sqlb.fields(&["o.path", "o.description", "o.resource_type"]); sqlb.and_where("o.workspace_id = ?".bind(&workspace_id)); sqlb.and_where("o.resource_type = ?".bind(&resource_type)); let sql = sqlb.sql().map_err(|_e| { tracing::error!("failed to build sql: {}", _e); - Error::internal_error("failed to build sql", None) + ErrorData::internal_error("failed to build sql", None) })?; let mut tx = user_db .clone() .begin(authed) .await - .map_err(|_e| Error::internal_error("failed to begin transaction", None))?; + .map_err(|_e| ErrorData::internal_error("failed to begin transaction", None))?; let rows = sqlx::query_as::<_, ResourceInfo>(&sql) .fetch_all(&mut *tx) .await .map_err(|_e| { tracing::error!("Failed to fetch resources: {}", _e); - Error::internal_error("failed to fetch resources", None) + ErrorData::internal_error("failed to fetch resources", None) })?; tx.commit() .await - .map_err(|_e| Error::internal_error("failed to commit transaction", None))?; + .map_err(|_e| ErrorData::internal_error("failed to commit transaction", None))?; Ok(rows) } @@ -138,7 +142,7 @@ pub async fn get_items sqlx::FromRow<'a, sqlx::postgres::PgRow> + Sen scope_type: &str, item_type: &str, scope_path: Option<&str>, -) -> Result, Error> { +) -> Result, ErrorData> { let mut sqlb = SqlBuilder::select_from(&format!("{} as o", item_type)); let fields = vec!["o.path", "o.summary", "o.description", "o.schema"]; sqlb.fields(&fields); @@ -157,9 +161,15 @@ pub async fn get_items sqlx::FromRow<'a, sqlx::postgres::PgRow> + Sen // scope path is always a folder path, format is f/my_folder/* if let Some(scope_path) = scope_path { - if scope_path.split("/").count() != 3 || !scope_path.starts_with("f/") || !scope_path.ends_with("/*") { - return Err(Error::internal_error( - format!("Invalid folder format: {}, expected format is f/my_folder/*", scope_path), + if scope_path.split("/").count() != 3 + || !scope_path.starts_with("f/") + || !scope_path.ends_with("/*") + { + return Err(ErrorData::internal_error( + format!( + "Invalid folder format: {}, expected format is f/my_folder/*", + scope_path + ), None, )); } @@ -177,23 +187,23 @@ pub async fn get_items sqlx::FromRow<'a, sqlx::postgres::PgRow> + Sen .limit(100); let sql = sqlb.sql().map_err(|_e| { tracing::error!("failed to build sql: {}", _e); - Error::internal_error("failed to build sql", None) + ErrorData::internal_error("failed to build sql", None) })?; let mut tx = user_db .clone() .begin(authed) .await - .map_err(|_e| Error::internal_error("failed to begin transaction", None))?; + .map_err(|_e| ErrorData::internal_error("failed to begin transaction", None))?; let rows = sqlx::query_as::<_, T>(&sql) .fetch_all(&mut *tx) .await .map_err(|_e| { tracing::error!("Failed to fetch {}: {}", item_type, _e); - Error::internal_error(format!("failed to fetch {}", item_type), None) + ErrorData::internal_error(format!("failed to fetch {}", item_type), None) })?; tx.commit() .await - .map_err(|_e| Error::internal_error("failed to commit transaction", None))?; + .map_err(|_e| ErrorData::internal_error("failed to commit transaction", None))?; Ok(rows) } @@ -201,7 +211,7 @@ pub async fn get_items sqlx::FromRow<'a, sqlx::postgres::PgRow> + Sen pub async fn get_scripts_from_hub( db: &DB, scope_integrations: Option<&str>, -) -> Result, Error> { +) -> Result, ErrorData> { let query_params = Some(vec![ ("limit", "100".to_string()), ("with_schema", "true".to_string()), @@ -213,34 +223,34 @@ pub async fn get_scripts_from_hub( .await .map_err(|e| { tracing::error!("Failed to get items from hub: {}", e); - Error::internal_error(format!("Failed to get items from hub: {}", e), None) + ErrorData::internal_error(format!("Failed to get items from hub: {}", e), None) })?; - + use axum::body::to_bytes; let body_bytes = to_bytes(response, usize::MAX).await.map_err(|e| { tracing::error!("Failed to read response body: {}", e); - Error::internal_error(format!("Failed to read response body: {}", e), None) + ErrorData::internal_error(format!("Failed to read response body: {}", e), None) })?; let body_str = String::from_utf8(body_bytes.to_vec()).map_err(|e| { tracing::error!("Failed to decode response body: {}", e); - Error::internal_error(format!("Failed to decode response body: {}", e), None) + ErrorData::internal_error(format!("Failed to decode response body: {}", e), None) })?; let hub_response: HubResponse = serde_json::from_str(&body_str).map_err(|e| { tracing::error!("Failed to parse hub response: {}", e); - Error::internal_error(format!("Failed to parse hub response: {}", e), None) + ErrorData::internal_error(format!("Failed to parse hub response: {}", e), None) })?; Ok(hub_response.asks) } /// Get the schema for a Hub script -pub async fn get_hub_script_schema(path: &str, db: &DB) -> Result, Error> { +pub async fn get_hub_script_schema(path: &str, db: &DB) -> Result, ErrorData> { let strip_path = StripPath(path.to_string()); let res = get_full_hub_script_by_path(strip_path, &HTTP_CLIENT, Some(db)) .await .map_err(|e| { tracing::error!("Failed to get hub script: {}", e); - Error::internal_error(format!("Failed to get hub script: {}", e), None) + ErrorData::internal_error(format!("Failed to get hub script: {}", e), None) })?; match serde_json::from_str::(res.schema.get()) { Ok(schema) => Ok(Some(schema)), @@ -249,4 +259,4 @@ pub async fn get_hub_script_schema(path: &str, db: &DB) -> Result Ok(None) } } -} \ No newline at end of file +} diff --git a/backend/windmill-api/src/mcp/utils/schema.rs b/backend/windmill-api/src/mcp/utils/schema.rs index b11f2f83ab..0de27a677f 100644 --- a/backend/windmill-api/src/mcp/utils/schema.rs +++ b/backend/windmill-api/src/mcp/utils/schema.rs @@ -3,16 +3,16 @@ //! Contains functions for transforming Windmill schemas into MCP-compatible formats, //! including resource enrichment and schema conversion utilities. -use rmcp::Error; +use rmcp::ErrorData; use serde_json::Value; use std::collections::HashMap; use windmill_common::db::UserDB; use windmill_common::scripts::Schema; -use crate::db::ApiAuthed; -use super::models::{SchemaType, ResourceInfo, ResourceType}; use super::database::get_resources; +use super::models::{ResourceInfo, ResourceType, SchemaType}; use super::transform::apply_key_transformation; +use crate::db::ApiAuthed; /// Convert a Windmill Schema to a SchemaType pub fn convert_schema_to_schema_type(schema: Option) -> SchemaType { @@ -35,7 +35,7 @@ pub async fn transform_schema_for_resources( w_id: &str, resources_cache: &mut HashMap>, resources_types: &Vec, -) -> Result { +) -> Result { let mut schema_obj: SchemaType = schema.clone(); // replace invalid char in property key with underscore @@ -71,24 +71,15 @@ pub async fn transform_schema_for_resources( let resource_type_obj = resource_type.cloned(); if !resources_cache.contains_key(&resource_type_key) { - let available_resources = get_resources( - user_db, - authed, - &w_id, - &resource_type_key, - ) - .await; + let available_resources = + get_resources(user_db, authed, &w_id, &resource_type_key).await; match available_resources { Ok(cache_data) => { - resources_cache - .insert(resource_type_key.clone(), cache_data); + resources_cache.insert(resource_type_key.clone(), cache_data); } Err(e) => { - tracing::error!( - "Failed to fetch resource cache data: {}", - e - ); + tracing::error!("Failed to fetch resource cache data: {}", e); continue; // Skip this property if fetching failed } } @@ -111,24 +102,16 @@ pub async fn transform_schema_for_resources( ), None => "An object parameter.".to_string() }; - prop_map.insert( - "type".to_string(), - Value::String("string".to_string()), - ); - prop_map.insert( - "description".to_string(), - Value::String(description), - ); + prop_map + .insert("type".to_string(), Value::String("string".to_string())); + prop_map.insert("description".to_string(), Value::String(description)); if resources_count > 0 { let resources_description = resource_cache .iter() .map(|resource| { format!( "{}: $res:{}", - resource - .description - .as_deref() - .unwrap_or("No title"), + resource.description.as_deref().unwrap_or("No title"), resource.path ) }) @@ -157,4 +140,4 @@ pub async fn transform_schema_for_resources( } Ok(schema_obj) -} \ No newline at end of file +} diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index 711de8f68d..823240a801 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -1808,7 +1808,7 @@ async fn edit_error_handler( SET error_handler = NULL, error_handler_extra_args = NULL, - error_handler_muted_on_cancel = NULL + error_handler_muted_on_cancel = false WHERE workspace_id = $1 "#, diff --git a/backend/windmill-worker/src/python_executor.rs b/backend/windmill-worker/src/python_executor.rs index 57936969b8..b31de6e184 100644 --- a/backend/windmill-worker/src/python_executor.rs +++ b/backend/windmill-worker/src/python_executor.rs @@ -2054,6 +2054,13 @@ pub async fn handle_python_reqs( .unwrap_or(Err(anyhow!("Problem by joining handle"))) { failed = true; + append_logs( + &job_id, + w_id, + format!("\nEnv installation failed: {:?}", e), + conn, + ) + .await; tracing::warn!( workspace_id = %w_id, "Env installation failed: {:?}", diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index 4371d3aacf..617d849b16 100644 --- a/benchmarks/lib.ts +++ b/benchmarks/lib.ts @@ -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.545.0"; +export const VERSION = "v1.546.1"; export async function login(email: string, password: string): Promise { return await windmill.UserService.login({ diff --git a/cli/src/commands/flow/flow.ts b/cli/src/commands/flow/flow.ts index 93938e6254..b6a59f95e1 100644 --- a/cli/src/commands/flow/flow.ts +++ b/cli/src/commands/flow/flow.ts @@ -14,7 +14,6 @@ import { FSFSElement, elementsToMap, ignoreF } from "../sync/sync.ts"; import { Flow } from "../../../gen/types.gen.ts"; import { replaceInlineScripts } from "../../../windmill-utils-internal/src/inline-scripts/replacer.ts"; - export interface FlowFile { summary: string; description?: string; @@ -55,7 +54,7 @@ export async function pushFlow( async (path: string) => await Deno.readTextFile(localPath + path), log, localPath, - SEP, + SEP ); if (flow) { @@ -191,7 +190,7 @@ async function run( workspace: workspace.workspaceId, id, }); - log.info(jobInfo.result ?? {}); + log.info(JSON.stringify(jobInfo.result ?? {}, null, 2)); } async function generateLocks( @@ -201,14 +200,23 @@ async function generateLocks( } & SyncOptions, folder: string | undefined ) { - const useRawReqs = opts.useRawRequirements || Deno.env.get("USE_RAW_REQUIREMENTS") === "true"; + const useRawReqs = + opts.useRawRequirements || Deno.env.get("USE_RAW_REQUIREMENTS") === "true"; const workspace = await resolveWorkspace(opts); await requireLogin(opts); opts = await mergeConfigWithConfigFile(opts); if (folder) { // read script metadata file - await generateFlowLockInternal(folder, false, workspace, opts, undefined, undefined, useRawReqs); + await generateFlowLockInternal( + folder, + false, + workspace, + opts, + undefined, + undefined, + useRawReqs + ); } else { const ignore = await ignoreF(opts); const elems = Object.keys( @@ -229,7 +237,15 @@ async function generateLocks( let hasAny = false; for (const folder of elems) { - const candidate = await generateFlowLockInternal(folder, true, workspace, opts, undefined, undefined, useRawReqs); + const candidate = await generateFlowLockInternal( + folder, + true, + workspace, + opts, + undefined, + undefined, + useRawReqs + ); if (candidate) { hasAny = true; log.info(colors.green(`+ ${candidate}`)); @@ -251,7 +267,15 @@ async function generateLocks( return; } for (const folder of elems) { - await generateFlowLockInternal(folder, false, workspace, opts,undefined, undefined, useRawReqs); + await generateFlowLockInternal( + folder, + false, + workspace, + opts, + undefined, + undefined, + useRawReqs + ); } } } diff --git a/cli/src/commands/script/script.ts b/cli/src/commands/script/script.ts index 9c4171a97e..f719a008a3 100644 --- a/cli/src/commands/script/script.ts +++ b/cli/src/commands/script/script.ts @@ -29,7 +29,7 @@ import { parseMetadataFile, } from "../../utils/metadata.ts"; import { - LanguageWithRawReqsSupport, + LanguageWithRawReqsSupport, ScriptLanguage, inferContentTypeFromFilePath, languagesWithRawReqsSupport, @@ -114,8 +114,14 @@ export async function findResourceFile(path: string) { if (currentBranch) { // Add branch-specific candidates at the beginning (higher priority) - const branchSpecificJSON = specificItems.toBranchSpecificPath(contentBasePathJSON, currentBranch); - const branchSpecificYAML = specificItems.toBranchSpecificPath(contentBasePathYAML, currentBranch); + const branchSpecificJSON = specificItems.toBranchSpecificPath( + contentBasePathJSON, + currentBranch + ); + const branchSpecificYAML = specificItems.toBranchSpecificPath( + contentBasePathYAML, + currentBranch + ); candidates.unshift(branchSpecificJSON, branchSpecificYAML); } @@ -624,7 +630,7 @@ export const exts = [ ".nu", ".playbook.yml", ".java", - ".rb" + ".rb", // for related places search: ADD_NEW_LANG ]; @@ -727,7 +733,7 @@ async function run( if (opts.silent) { console.log(result); } else { - log.info(result); + log.info(JSON.stringify(result, null, 2)); } break; @@ -885,7 +891,10 @@ async function bootstrap( ); } -export type GlobalDeps = Map>; +export type GlobalDeps = Map< + LanguageWithRawReqsSupport, + Record +>; export async function findGlobalDeps(): Promise { var globalDeps: GlobalDeps = new Map(); @@ -895,9 +904,8 @@ export async function findGlobalDeps(): Promise { return ( !isDir && // Skip if the filename is not one of lockfile names - !(languagesWithRawReqsSupport.some( - lockfile => - p.endsWith(SEP + lockfile.rrFilename)) + !languagesWithRawReqsSupport.some((lockfile) => + p.endsWith(SEP + lockfile.rrFilename) ) ); }, els)) { @@ -906,9 +914,11 @@ export async function findGlobalDeps(): Promise { // Iterate over available languages to find which lockfile languagesWithRawReqsSupport.map((lock) => { - if (entry.path.endsWith(lock.rrFilename)){ + if (entry.path.endsWith(lock.rrFilename)) { const current = globalDeps.get(lock) ?? {}; - current[entry.path.substring(0, entry.path.length - lock.rrFilename.length)] = content; + current[ + entry.path.substring(0, entry.path.length - lock.rrFilename.length) + ] = content; globalDeps.set(lock, current); } }); diff --git a/cli/src/core/settings.ts b/cli/src/core/settings.ts index 00b69128fd..c31a4d6338 100644 --- a/cli/src/core/settings.ts +++ b/cli/src/core/settings.ts @@ -78,7 +78,7 @@ export async function pushWorkspaceSettings( error_handler: remoteSettings.error_handler, error_handler_extra_args: remoteSettings.error_handler_extra_args, error_handler_muted_on_cancel: - remoteSettings.error_handler_muted_on_cancel, + remoteSettings.error_handler_muted_on_cancel ?? false, ai_config: remoteSettings.ai_config, large_file_storage: remoteSettings.large_file_storage, git_sync: remoteSettings.git_sync, @@ -166,8 +166,8 @@ export async function pushWorkspaceSettings( localSettings.error_handler_extra_args, settings.error_handler_extra_args ) || - localSettings.error_handler_muted_on_cancel != - settings.error_handler_muted_on_cancel + (localSettings.error_handler_muted_on_cancel ?? false) != + (settings.error_handler_muted_on_cancel ?? false) ) { log.debug(`Updating error handler...`); await wmill.editErrorHandler({ @@ -176,7 +176,7 @@ export async function pushWorkspaceSettings( error_handler: localSettings.error_handler, error_handler_extra_args: localSettings.error_handler_extra_args, error_handler_muted_on_cancel: - localSettings.error_handler_muted_on_cancel, + localSettings.error_handler_muted_on_cancel ?? false, }, }); } diff --git a/cli/src/main.ts b/cli/src/main.ts index 034fdbeb1a..baf76a3dba 100644 --- a/cli/src/main.ts +++ b/cli/src/main.ts @@ -68,7 +68,7 @@ export { // } // }); -export const VERSION = "1.545.0"; +export const VERSION = "1.546.1"; export const WM_FORK_PREFIX = "wm-fork"; @@ -187,7 +187,7 @@ async function main() { log.setup({ handlers: { console: new log.ConsoleHandler(LOG_LEVEL, { - formatter: ({ msg }) => `${msg}`, + formatter: ({ msg }) => msg, useColors: isWin ? false : true, }), }, diff --git a/cli/src/types.ts b/cli/src/types.ts index ec3600b0a8..a7b821882a 100644 --- a/cli/src/types.ts +++ b/cli/src/types.ts @@ -301,18 +301,30 @@ export function getTypeStrFromPath( } export function removeType(str: string, type: string) { + // Normalize path for cross-platform compatibility and convert to forward slashes for API consistency + const normalizedStr = path.normalize(str).replaceAll(SEP, "/"); + if ( - !str.endsWith("." + type + ".yaml") && - !str.endsWith("." + type + ".json") + !normalizedStr.endsWith("." + type + ".yaml") && + !normalizedStr.endsWith("." + type + ".json") ) { throw new Error(str + " does not end with ." + type + ".(yaml|json)"); } - return str.slice(0, str.length - type.length - 6); + return normalizedStr.slice(0, normalizedStr.length - type.length - 6); } export function removePathPrefix(str: string, prefix: string) { - if (!str.startsWith(prefix + "/")) { + // Normalize paths for cross-platform compatibility and convert to forward slashes for API consistency + const normalizedStr = path.normalize(str).replaceAll(SEP, "/"); + const normalizedPrefix = path.normalize(prefix).replaceAll(SEP, "/"); + + // Handle exact match case + if (normalizedStr === normalizedPrefix) { + return ""; + } + + if (!normalizedStr.startsWith(normalizedPrefix + "/")) { throw new Error(str + " does not start with " + prefix); } - return str.slice(prefix.length + 1); + return normalizedStr.slice(normalizedPrefix.length + 1); } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 648f12a6c8..5f2f72761b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-components", - "version": "1.545.0", + "version": "1.546.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-components", - "version": "1.545.0", + "version": "1.546.1", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/frontend/package.json b/frontend/package.json index 3946274a02..4a8441970f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "windmill-components", - "version": "1.545.0", + "version": "1.546.1", "scripts": { "dev": "vite dev", "build": "vite build", diff --git a/frontend/src/lib/components/DynamicInput.svelte b/frontend/src/lib/components/DynamicInput.svelte index 23d2912052..0a11e4663c 100644 --- a/frontend/src/lib/components/DynamicInput.svelte +++ b/frontend/src/lib/components/DynamicInput.svelte @@ -36,14 +36,16 @@ let { value = $bindable(), helperScript, format, otherArgs: otherArgs }: Props = $props() - const [inputType, entrypoint] = format.includes('-') ? format.split('-', 2) : [format, ''] + let [inputType, entrypoint] = $derived(format.includes('-') ? format.split('-', 2) : [format, '']) - const isMultiple = inputType === 'dynmultiselect' - const isSelect = inputType === 'dynselect' || inputType === 'dynmultiselect' + let isMultiple = $derived(inputType === 'dynmultiselect') + let isSelect = $derived(inputType === 'dynselect' || inputType === 'dynmultiselect') - if (isMultiple && value === undefined) { - value = [] - } + $effect.pre(() => { + if (isMultiple && value === undefined) { + value = [] + } + }) let resultJobLoader: JobLoader | undefined = $state() let _items = usePromise(getItemsFromOptions, { clearValueOnRefresh: false }) @@ -97,7 +99,7 @@ $effect(() => { if (_items.value && value !== undefined && isSelect) { - if (isMultiple && Array.isArray(value)) { + if (isMultiple && Array.isArray(value) && Array.isArray(_items.value)) { const availableValues = new Set(_items.value.map((x) => x.value)) const filteredValue = value.filter((v) => availableValues.has(v)) if (filteredValue.length !== value.length) { diff --git a/frontend/src/lib/components/EditableSchemaForm.svelte b/frontend/src/lib/components/EditableSchemaForm.svelte index 9c91888960..a138a6cd1d 100644 --- a/frontend/src/lib/components/EditableSchemaForm.svelte +++ b/frontend/src/lib/components/EditableSchemaForm.svelte @@ -372,6 +372,14 @@ } function updateDynCode(functionName: string, lang: ScriptLang = 'bun') { + if ( + (lang == 'bun' && dynCode?.includes(`function ${functionName}`)) || + (lang == 'python3' && dynCode?.includes(`def ${functionName}`)) + ) { + // Don't add the function if it already exists + return + } + const generateFn = DynamicInput.getGenerateTemplateFn(lang) const code = generateFn(functionName) dynCode = dynCode ? dynCode.concat(code) : code @@ -701,6 +709,15 @@ nullable: undefined, required: undefined } + + if ( + isDynMultiselect && + args && + !Array.isArray(args?.[argName]) + ) { + args[argName] = [] + } + if (isS3) { schema.properties[argName] = { ...emptyProperty, diff --git a/frontend/src/lib/components/InputTransformForm.svelte b/frontend/src/lib/components/InputTransformForm.svelte index c14a89da7b..f91e1880ae 100644 --- a/frontend/src/lib/components/InputTransformForm.svelte +++ b/frontend/src/lib/components/InputTransformForm.svelte @@ -9,7 +9,7 @@ {#each Object.keys(components['aggridinfinitecomponent'].initialData.configuration) as key (key)} @@ -194,10 +197,17 @@ bind:result bind:loading bind:runnableComponent - on:recompute={() => { - console.log('recompute') - clear() - }} + preventDefaultRefresh + overrideCallback={() => + new CancelablePromise(async (resolve) => { + if (ignoreFirst) { + ignoreFirst = false + resolve() + return + } + clear() + resolve() + })} {render} autoRefresh={true} allowConcurentRequests diff --git a/frontend/src/lib/components/apps/components/display/table/SyncColumnDefs.svelte b/frontend/src/lib/components/apps/components/display/table/SyncColumnDefs.svelte index 0b9977d948..9e45c0f770 100644 --- a/frontend/src/lib/components/apps/components/display/table/SyncColumnDefs.svelte +++ b/frontend/src/lib/components/apps/components/display/table/SyncColumnDefs.svelte @@ -118,11 +118,12 @@ } }) + let keys = $derived(Object.keys(result[0] ?? []).filter((x) => x !== '__index')) + async function syncColumns() { const gridItem = findGridItem($app, id) if (gridItem && result) { - const keys = Object.keys(result[0] ?? {}) ?? [] const conf = gridItem.data.configuration.columnDefs as ColumnDefsConfiguration const newColumns: WindmillColumnDef[] = keys.map((key) => ({ @@ -171,7 +172,7 @@
No columns definition found. Columns found in data:
- {#each Object.keys(result[0] ?? []) as key} + {#each keys as key} {key} {/each}
diff --git a/frontend/src/lib/components/apps/components/helpers/RefreshButton.svelte b/frontend/src/lib/components/apps/components/helpers/RefreshButton.svelte index fc25b2e382..14729e30fe 100644 --- a/frontend/src/lib/components/apps/components/helpers/RefreshButton.svelte +++ b/frontend/src/lib/components/apps/components/helpers/RefreshButton.svelte @@ -32,13 +32,17 @@ color="light" size="xs2" btnClasses={twMerge(loading ? ' bg-blue-100 dark:bg-blue-400' : '', 'transition-all')} - on:click={() => { + on:click={(e) => { if (buttonHover && loading) { cancelCallbacks?.forEach((cb) => cb.cancel()) } else { cancelCallbacks = $runnableComponents[id]?.cb?.map((cb) => cb()) } }} + on:pointerdown={(e) => { + e.preventDefault() + e.stopPropagation() + }} iconOnly /> {#snippet text()} diff --git a/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte b/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte index 3b6eb73854..29780ce291 100644 --- a/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte +++ b/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte @@ -163,48 +163,8 @@ } } - // $: sendUserToast('args' + JSON.stringify(runnableInputValues) + Boolean(extraQueryParams) || args) - // $: console.log(runnableInputValues) let firstRefresh = true - // on:started={(e) => { - // console.log('started', e.detail) - // loading = true - // setJobId(e.detail) - // dispatch('started', e.detail) - // }} - // on:done={(e) => { - // lastJobId = e.detail.id - // setResult(e.detail.result, e.detail.id) - // loading = false - // dispatch('done', { id: e.detail?.id, result: e.detail?.result }) - // }} - // on:cancel={(e) => { - // let jobId = e.detail - // console.debug('cancel', jobId) - // let job = $jobsById[jobId] - // if (job && job.created_at && !job.duration_ms) { - // $jobsById[jobId] = { - // ...job, - // started_at: job.started_at ?? Date.now(), - // duration_ms: Date.now() - (job.started_at ?? job.created_at) - // } - // } - // dispatch('cancel', { id: e.detail }) - // }} - // on:running={(e) => { - // let jobId = e.detail - // let job = $jobsById[jobId] - // if (job && !job.started_at) { - // $jobsById[jobId] = { ...job, started_at: Date.now() } - // } - // }} - // on:doneError={(e) => { - // setResult({ error: e.detail.error }, e.detail.id) - // loading = false - // dispatch('doneError', { id: e.detail.id, result: e.detail.result }) - // }} - type RunnableCallback = { onDone?: (r: any) => void onCancel?: () => void diff --git a/frontend/src/lib/components/apps/components/helpers/RunnableWrapper.svelte b/frontend/src/lib/components/apps/components/helpers/RunnableWrapper.svelte index 7f4dd7856a..2874422d2f 100644 --- a/frontend/src/lib/components/apps/components/helpers/RunnableWrapper.svelte +++ b/frontend/src/lib/components/apps/components/helpers/RunnableWrapper.svelte @@ -91,6 +91,7 @@ onSuccess?: (result: any) => void children?: import('svelte').Snippet nonRenderedPlaceholder?: import('svelte').Snippet + preventDefaultRefresh?: boolean } let { diff --git a/frontend/src/lib/components/common/layout/List.svelte b/frontend/src/lib/components/common/layout/List.svelte index 0d57af31cd..d1be35d1ce 100644 --- a/frontend/src/lib/components/common/layout/List.svelte +++ b/frontend/src/lib/components/common/layout/List.svelte @@ -1,10 +1,22 @@