diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fdce702331..e47ce1f0a3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.830.0" + ".": "1.833.0" } diff --git a/AGENTS.md b/AGENTS.md index a57215cde5..8c30fed2f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,8 +34,9 @@ Open-source platform for internal tools, workflows, API integrations, background lifecycle lock, managed-object markers, the setup gate, per-cluster roles, fork copy ownership) and how to run one locally - **Data table viewer roles**: `docs/datatable-viewer-roles.md`, on what a connection naming no - role holds: the default role as a floor, where the viewer comes from, why an admin's roles are - not the viewer's, the per-role-set logins and the order their rows and logins are written in + role holds: the default role as a floor, whose roles are added (the job's own, or its viewer's + for an inline app runnable that opts in), why an admin's roles are not added, the per-role-set + logins and the order their rows and logins are written in - **Enterprise**: `docs/enterprise.md` — EE file conventions and PR workflow - **Operator write rights**: `docs/operator-write-rights.md` — which `operator_settings` flags are enforced rather than cosmetic, and why a right that is granted-unless-withdrawn needs `Option` diff --git a/CHANGELOG.md b/CHANGELOG.md index 80939b1b47..0edb6b5dcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,83 @@ # Changelog +## [1.833.0](https://github.com/windmill-labs/windmill/compare/v1.832.0...v1.833.0) (2026-10-13) + + +### Features + +* make the assistant aware of the user's role permissions ([#11722](https://github.com/windmill-labs/windmill/issues/11722)) ([633135b](https://github.com/windmill-labs/windmill/commit/633135bb917ce1be711a75991531c3e478d73463)) +* read data as the job's identity, viewer is opt-in per runnable ([#11744](https://github.com/windmill-labs/windmill/issues/11744)) ([b7b3306](https://github.com/windmill-labs/windmill/commit/b7b33061952fde247447fa652663ffd730990d5e)) +* show a raw app's document.title in the page tab ([#11747](https://github.com/windmill-labs/windmill/issues/11747)) ([5153a5e](https://github.com/windmill-labs/windmill/commit/5153a5e7b81e9d56ff860129be7c49f444c3c4e2)) +* shrink the page header in stages and fold page actions by measured width ([#11732](https://github.com/windmill-labs/windmill/issues/11732)) ([3857ce3](https://github.com/windmill-labs/windmill/commit/3857ce36931dff40322db2b765ab1e65763eac42)) + + +### Bug Fixes + +* encrypt ai session backups with aes-256-gcm ([#11734](https://github.com/windmill-labs/windmill/issues/11734)) ([c7ee201](https://github.com/windmill-labs/windmill/commit/c7ee2016559b8083c83c696c41d226d9ca04ed68)) +* error handler picker keeps no per-tab state ([#11742](https://github.com/windmill-labs/windmill/issues/11742)) ([2f9a111](https://github.com/windmill-labs/windmill/commit/2f9a111bbb14a40e1341879e1ef63c3457e7a08d)) +* show the email handler's SMTP status to non-superadmin admins ([#11745](https://github.com/windmill-labs/windmill/issues/11745)) ([130adfd](https://github.com/windmill-labs/windmill/commit/130adfdd20af0e30d87986cf77b61023a9732186)) + +## [1.832.0](https://github.com/windmill-labs/windmill/compare/v1.831.0...v1.832.0) (2026-10-13) + + +### Features + +* record each instance's python version and start new ones on 3.13 ([#11656](https://github.com/windmill-labs/windmill/issues/11656)) ([7558ce2](https://github.com/windmill-labs/windmill/commit/7558ce27a9736bef1a9b9db9b0799150dccb11a5)) + + +### Bug Fixes + +* badge the unavailable sign-in option in the oauth resource form ([#11728](https://github.com/windmill-labs/windmill/issues/11728)) ([9c44f5a](https://github.com/windmill-labs/windmill/commit/9c44f5a917fc628e870073c126bf9480c98c7f73)) +* serve s3 proxy objects with nosniff and a sandbox csp ([#11731](https://github.com/windmill-labs/windmill/issues/11731)) ([e29a0dc](https://github.com/windmill-labs/windmill/commit/e29a0dc5dfed4bb62b894e059c9506873d32a62c)) +* widen variable.value without breaking live servers' cached queries ([#11737](https://github.com/windmill-labs/windmill/issues/11737)) ([89e6bdb](https://github.com/windmill-labs/windmill/commit/89e6bdbcf50e065f6906e1ab93b3c4109aba3ab6)) + +## [1.831.0](https://github.com/windmill-labs/windmill/compare/v1.830.0...v1.831.0) (2026-10-13) + + +### Features + +* add python 3.14 and 3.15 to the supported instance versions ([#11654](https://github.com/windmill-labs/windmill/issues/11654)) ([74c9b0c](https://github.com/windmill-labs/windmill/commit/74c9b0cb2eebc16dd5e23f23be89b127bcbe6ec2)) +* add the viewer's roles to unnamed data table connections ([#11709](https://github.com/windmill-labs/windmill/issues/11709)) ([a298ed3](https://github.com/windmill-labs/windmill/commit/a298ed3fc19a1b7fc4f8e75a95c8d38652b74b5b)) +* audit declarative instance config writes ([#11684](https://github.com/windmill-labs/windmill/issues/11684)) ([afbbc0b](https://github.com/windmill-labs/windmill/commit/afbbc0b1b2da1e468b10d4cd11e9f3ac7478a34d)) +* **cli:** declare app sandbox and frontend api scopes in app yaml ([#11703](https://github.com/windmill-labs/windmill/issues/11703)) ([26afeec](https://github.com/windmill-labs/windmill/commit/26afeecbd5b0a11c084813182e92805a15d21bbb)) +* data table migration tools in ai sessions ([#11723](https://github.com/windmill-labs/windmill/issues/11723)) ([3d74f6e](https://github.com/windmill-labs/windmill/commit/3d74f6ea2a44e85d056411c293087741f6f1e12a)) +* encrypt workspace secrets with AES-256-GCM ([#11678](https://github.com/windmill-labs/windmill/issues/11678)) ([4942149](https://github.com/windmill-labs/windmill/commit/4942149b05c9384b39402f0e3cb7771565888f45)) +* external signer for OIDC tokens and a log line per mint ([#11614](https://github.com/windmill-labs/windmill/issues/11614)) ([157183a](https://github.com/windmill-labs/windmill/commit/157183ae9cad4b6b2d38f1f4d915f32464792453)) +* give app builder operators app tools in AI sessions ([#11716](https://github.com/windmill-labs/windmill/issues/11716)) ([133613c](https://github.com/windmill-labs/windmill/commit/133613c1add5b6fcdbb0937de6479976b716c90a)) +* redesign the s3 explorer and show data files as tables ([#11642](https://github.com/windmill-labs/windmill/issues/11642)) ([9fa9b77](https://github.com/windmill-labs/windmill/commit/9fa9b7792676fbedcbc4dc37ebb883f5ff5abb35)) +* support array arguments in snowflake scripts ([#11702](https://github.com/windmill-labs/windmill/issues/11702)) ([1451988](https://github.com/windmill-labs/windmill/commit/14519884e4c9aa4d0a006cbc019a776f9a038967)) + + +### Bug Fixes + +* bump git sync hub scripts to cli 1.830.0 ([#11681](https://github.com/windmill-labs/windmill/issues/11681)) ([7bbe032](https://github.com/windmill-labs/windmill/commit/7bbe03227d56688893aafda305a13d65b2bb84f4)) +* end other sessions when a user changes their password ([#11710](https://github.com/windmill-labs/windmill/issues/11710)) ([add042e](https://github.com/windmill-labs/windmill/commit/add042ef2f05828a8ea5f6d0a089204bae2322d5)) +* fill app ctx inputs from the viewer's session, not the request ([#11698](https://github.com/windmill-labs/windmill/issues/11698)) ([7627c1b](https://github.com/windmill-labs/windmill/commit/7627c1be8d82c298173ad4a7002483d1caee6662)) +* **groups:** recompute member roles when an instance group role is cleared ([#11708](https://github.com/windmill-labs/windmill/issues/11708)) ([4be67fe](https://github.com/windmill-labs/windmill/commit/4be67fe3ac09f722f31ef85e6fc757bb0a2fabe4)) +* hide the sidebar triggers header when no trigger entry is visible ([#11715](https://github.com/windmill-labs/windmill/issues/11715)) ([e443eb3](https://github.com/windmill-labs/windmill/commit/e443eb37079734469a78af8ff4bc0f4289b28d0a)) +* keep a custom error handler's slack argument on save ([#11696](https://github.com/windmill-labs/windmill/issues/11696)) ([c6692ac](https://github.com/windmill-labs/windmill/commit/c6692ace156f5295196af07015ad82aee6391116)) +* keep ai agent schema property order stable across jobs ([#11687](https://github.com/windmill-labs/windmill/issues/11687)) ([a8e2f1f](https://github.com/windmill-labs/windmill/commit/a8e2f1f1ca28e3352aff8a450188e3ff75a9637c)) +* keep preserve_step_tags on saved branch and loop flow nodes ([#11660](https://github.com/windmill-labs/windmill/issues/11660)) ([dedd775](https://github.com/windmill-labs/windmill/commit/dedd77587f3234f3e480388b00340394794ac5bb)) +* keep the app editor's parent_version out of deployed apps ([#11719](https://github.com/windmill-labs/windmill/issues/11719)) ([2c6de9d](https://github.com/windmill-labs/windmill/commit/2c6de9d896ef37993da974418133a88798afa2bb)) +* keep the resource type index whole when a refresh fails ([#11727](https://github.com/windmill-labs/windmill/issues/11727)) ([71caac7](https://github.com/windmill-labs/windmill/commit/71caac7aec49bafe8f691e8a82b3e6df84f7e4bc)) +* keep the resource type search current between daily fills, and serve it without an index ([#11712](https://github.com/windmill-labs/windmill/issues/11712)) ([a365359](https://github.com/windmill-labs/windmill/commit/a3653593f2b064b762a19a1d4294988c27baaec0)) +* load the s3 explorer table lazily to keep monaco out of the app shell ([#11701](https://github.com/windmill-labs/windmill/issues/11701)) ([a92b29d](https://github.com/windmill-labs/windmill/commit/a92b29dae6950d29c2b8970e07d055fa02ff979e)) +* parse python 3.12-3.15 syntax in the python parsers ([#11675](https://github.com/windmill-labs/windmill/issues/11675)) ([aae5f2e](https://github.com/windmill-labs/windmill/commit/aae5f2efaa772d5bc6d7d1e4537d7f6748e50e61)) +* persist hidden languages in edit default scripts drawer ([#11693](https://github.com/windmill-labs/windmill/issues/11693)) ([aeeeb04](https://github.com/windmill-labs/windmill/commit/aeeeb042f93df32214e00040d360f20e8ed8c98a)) +* pin the floating sidebar and let page clicks through it ([#11711](https://github.com/windmill-labs/windmill/issues/11711)) ([ce45ab5](https://github.com/windmill-labs/windmill/commit/ce45ab507370662266728bf57240cc873ef28eb8)) +* resolve $var: references in mcp resource headers ([#11679](https://github.com/windmill-labs/windmill/issues/11679)) ([478eef7](https://github.com/windmill-labs/windmill/commit/478eef735222e60de8ed6086f6b036e2346f1de4)) +* resolve a new item's chosen name in session preview tools ([#11688](https://github.com/windmill-labs/windmill/issues/11688)) ([cd502a5](https://github.com/windmill-labs/windmill/commit/cd502a5270e787aeb1e5cb14e5a0e0aad8bb97b3)) +* resolve variables in the url and headers of an mcp resource ([#11680](https://github.com/windmill-labs/windmill/issues/11680)) ([4bc2394](https://github.com/windmill-labs/windmill/commit/4bc2394322f957e5cf212fa1913b2b39c1b9ca06)) +* run a workflow-as-code task called from inside a task in place ([#11699](https://github.com/windmill-labs/windmill/issues/11699)) ([72d1092](https://github.com/windmill-labs/windmill/commit/72d1092de97d7449ff03c3a44835c9d23cfe5e02)) +* stop marking run form fields as disabled for assistive tech ([#11714](https://github.com/windmill-labs/windmill/issues/11714)) ([c7df009](https://github.com/windmill-labs/windmill/commit/c7df0093d0d9a38362222b7350bf387c31af9392)) +* stop session dom tools reading a raw app preview that is not current ([#11700](https://github.com/windmill-labs/windmill/issues/11700)) ([05c7b68](https://github.com/windmill-labs/windmill/commit/05c7b688e5e963c6c313cb5da8e777cee11cd122)) +* surface oauth provider errors returned with http 200 ([#11713](https://github.com/windmill-labs/windmill/issues/11713)) ([d8ac4f5](https://github.com/windmill-labs/windmill/commit/d8ac4f59c8d570777e43a7aa7ffbc97598897cc2)) + + +### Performance Improvements + +* run shellcheck directly from the lsp launcher ([#11707](https://github.com/windmill-labs/windmill/issues/11707)) ([8cdb42a](https://github.com/windmill-labs/windmill/commit/8cdb42aaae764977008f8ac4598709c7ae09af3f)) + ## [1.830.0](https://github.com/windmill-labs/windmill/compare/v1.829.0...v1.830.0) (2026-10-09) diff --git a/Dockerfile b/Dockerfile index c6a4ccc55f..4e022d4b53 100644 --- a/Dockerfile +++ b/Dockerfile @@ -152,11 +152,12 @@ ARG WITH_HELM=true ARG WITH_GIT=true ARG features="" -# To change latest stable version: -# 1. Change placeholder in instanceSettings.ts -# 2. Change LATEST_STABLE_PY in dockerfile -# 3. Change #[default] annotation for PyVersion in backend +# Instances keep the Python version they were initialized with, so both are preinstalled: +# LATEST_STABLE_PY is what instances initialized before the version was raised are on +# (PyVAlias::default() in the backend), NEW_INSTANCE_PY the one new instances start on +# (PyVAlias::NEW_INSTANCE). When raising the latter, keep installing every earlier value. ARG LATEST_STABLE_PY=3.12 +ARG NEW_INSTANCE_PY=3.13 ENV UV_PYTHON_INSTALL_DIR=/tmp/windmill/cache/py_runtime ENV UV_PYTHON_PREFERENCE=only-managed @@ -249,6 +250,7 @@ RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releas # timestamps or Python's mtime-based .pyc invalidation discards these compiled files. RUN UV_CACHE_DIR=/tmp/build_cache/uv UV_PYTHON_INSTALL_DIR=/tmp/build_cache/py_runtime uv python install 3.11 --compile-bytecode RUN UV_CACHE_DIR=/tmp/build_cache/uv UV_PYTHON_INSTALL_DIR=/tmp/build_cache/py_runtime uv python install $LATEST_STABLE_PY --compile-bytecode +RUN UV_CACHE_DIR=/tmp/build_cache/uv UV_PYTHON_INSTALL_DIR=/tmp/build_cache/py_runtime uv python install $NEW_INSTANCE_PY --compile-bytecode RUN curl -sL https://deb.nodesource.com/setup_24.x | bash - diff --git a/ai_evals/adapters/frontend/core/global/evalPreviewTabs.ts b/ai_evals/adapters/frontend/core/global/evalPreviewTabs.ts index f32bf1e22e..b6af04e6ec 100644 --- a/ai_evals/adapters/frontend/core/global/evalPreviewTabs.ts +++ b/ai_evals/adapters/frontend/core/global/evalPreviewTabs.ts @@ -17,6 +17,7 @@ import { previewLocationLabel, promptSafe, resolvePreviewTab, + slotShowsPage, } from "../../../../../frontend/src/lib/components/sessions/previewRouter"; import type { ArtifactVersionTarget } from "../../../../../frontend/src/lib/components/sessions/previewRouter"; import type { SessionTarget } from "../../../../../frontend/src/lib/components/sessions/sessionState.svelte"; @@ -136,9 +137,8 @@ export function createEvalPreviewPanel(input: { activePreview: () => { const tab = owner.displayedTab; if (!tab) return undefined; - // Artifact and editor tabs are not iframes: they carry no page location, and an - // artifact's pinned version reaches the chat only through get_preview_status. - if (resolvePreviewTab(tab.url).kind !== "iframe") return undefined; + // An artifact's pinned version reaches the chat only through get_preview_status. + if (!slotShowsPage(resolvePreviewTab(tab.url).kind)) return undefined; return previewLocationContext(whereIs(tab)); }, dispose: () => { diff --git a/backend/.sqlx/query-711927611ecf0144b6241b8f39f94b79f4ff03bdae60ab746f930f8758fe46bc.json b/backend/.sqlx/query-711927611ecf0144b6241b8f39f94b79f4ff03bdae60ab746f930f8758fe46bc.json new file mode 100644 index 0000000000..4735c589ac --- /dev/null +++ b/backend/.sqlx/query-711927611ecf0144b6241b8f39f94b79f4ff03bdae60ab746f930f8758fe46bc.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE job_perms SET viewer_data_access = true WHERE job_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "711927611ecf0144b6241b8f39f94b79f4ff03bdae60ab746f930f8758fe46bc" +} diff --git a/backend/.sqlx/query-9ec675bd675c9915ed56f6cb91ca848b094b61475409156489319cb9d36ef096.json b/backend/.sqlx/query-9ec675bd675c9915ed56f6cb91ca848b094b61475409156489319cb9d36ef096.json new file mode 100644 index 0000000000..bd3f00aa7d --- /dev/null +++ b/backend/.sqlx/query-9ec675bd675c9915ed56f6cb91ca848b094b61475409156489319cb9d36ef096.json @@ -0,0 +1,330 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n v2_job_queue.workspace_id,\n v2_job_queue.id,\n v2_job.args as \"args: sqlx::types::Json>>\",\n v2_job.parent_job,\n v2_job.created_by,\n v2_job_queue.started_at,\n v2_job_queue.runnable_settings_handle,\n scheduled_for,\n runnable_path,\n kind as \"kind: JobKind\",\n runnable_id as \"runnable_id: ScriptHash\",\n canceled_reason,\n canceled_by,\n permissioned_as,\n permissioned_as_email,\n flow_status as \"flow_status: sqlx::types::Json>\",\n v2_job.tag,\n script_lang as \"script_lang: ScriptLang\",\n same_worker,\n pre_run_error,\n concurrent_limit,\n concurrency_time_window_s,\n flow_innermost_root_job,\n root_job,\n timeout,\n flow_step_id,\n cache_ttl,\n cache_ignore_s3_path,\n v2_job_queue.priority,\n preprocessed,\n script_entrypoint_override,\n trigger,\n trigger_kind as \"trigger_kind: TriggerKindLabel\",\n visible_to_owner,\n NULL as permissioned_as_end_user_email,\n NULL::boolean as viewer_data_access,\n job_perms.job_token_scopes\n FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id\n LEFT JOIN job_perms ON job_perms.job_id = v2_job_queue.id WHERE v2_job_queue.id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "args: sqlx::types::Json>>", + "type_info": "Jsonb" + }, + { + "ordinal": 3, + "name": "parent_job", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "created_by", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "started_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "runnable_settings_handle", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "scheduled_for", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "runnable_path", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "kind: JobKind", + "type_info": { + "Custom": { + "name": "job_kind", + "kind": { + "Enum": [ + "script", + "preview", + "flow", + "dependencies", + "flowpreview", + "script_hub", + "identity", + "flowdependencies", + "http", + "graphql", + "postgresql", + "noop", + "appdependencies", + "deploymentcallback", + "singlestepflow", + "flowscript", + "flownode", + "appscript", + "aiagent", + "unassigned_script", + "unassigned_flow", + "unassigned_singlestepflow" + ] + } + } + } + }, + { + "ordinal": 10, + "name": "runnable_id: ScriptHash", + "type_info": "Int8" + }, + { + "ordinal": 11, + "name": "canceled_reason", + "type_info": "Text" + }, + { + "ordinal": 12, + "name": "canceled_by", + "type_info": "Varchar" + }, + { + "ordinal": 13, + "name": "permissioned_as", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "permissioned_as_email", + "type_info": "Varchar" + }, + { + "ordinal": 15, + "name": "flow_status: sqlx::types::Json>", + "type_info": "Jsonb" + }, + { + "ordinal": 16, + "name": "tag", + "type_info": "Varchar" + }, + { + "ordinal": 17, + "name": "script_lang: ScriptLang", + "type_info": { + "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", + "rlang", + "dbt" + ] + } + } + } + }, + { + "ordinal": 18, + "name": "same_worker", + "type_info": "Bool" + }, + { + "ordinal": 19, + "name": "pre_run_error", + "type_info": "Text" + }, + { + "ordinal": 20, + "name": "concurrent_limit", + "type_info": "Int4" + }, + { + "ordinal": 21, + "name": "concurrency_time_window_s", + "type_info": "Int4" + }, + { + "ordinal": 22, + "name": "flow_innermost_root_job", + "type_info": "Uuid" + }, + { + "ordinal": 23, + "name": "root_job", + "type_info": "Uuid" + }, + { + "ordinal": 24, + "name": "timeout", + "type_info": "Int4" + }, + { + "ordinal": 25, + "name": "flow_step_id", + "type_info": "Varchar" + }, + { + "ordinal": 26, + "name": "cache_ttl", + "type_info": "Int4" + }, + { + "ordinal": 27, + "name": "cache_ignore_s3_path", + "type_info": "Bool" + }, + { + "ordinal": 28, + "name": "priority", + "type_info": "Int2" + }, + { + "ordinal": 29, + "name": "preprocessed", + "type_info": "Bool" + }, + { + "ordinal": 30, + "name": "script_entrypoint_override", + "type_info": "Varchar" + }, + { + "ordinal": 31, + "name": "trigger", + "type_info": "Varchar" + }, + { + "ordinal": 32, + "name": "trigger_kind: TriggerKindLabel", + "type_info": { + "Custom": { + "name": "job_trigger_kind", + "kind": { + "Enum": [ + "webhook", + "http", + "websocket", + "kafka", + "email", + "nats", + "schedule", + "app", + "ui", + "postgres", + "sqs", + "gcp", + "mqtt", + "nextcloud", + "google", + "ci_test", + "github", + "azure", + "asset", + "freshness", + "amqp" + ] + } + } + } + }, + { + "ordinal": 33, + "name": "visible_to_owner", + "type_info": "Bool" + }, + { + "ordinal": 34, + "name": "permissioned_as_end_user_email", + "type_info": "Text" + }, + { + "ordinal": 35, + "name": "viewer_data_access", + "type_info": "Bool" + }, + { + "ordinal": 36, + "name": "job_token_scopes", + "type_info": "TextArray" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + true, + true, + false, + true, + true, + false, + true, + false, + true, + true, + true, + false, + false, + true, + false, + true, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false, + null, + null, + true + ] + }, + "hash": "9ec675bd675c9915ed56f6cb91ca848b094b61475409156489319cb9d36ef096" +} diff --git a/backend/.sqlx/query-fa62a55dd0a635995390ad4189b847b91388b6699f16afb57d8147d7e9712220.json b/backend/.sqlx/query-fa62a55dd0a635995390ad4189b847b91388b6699f16afb57d8147d7e9712220.json new file mode 100644 index 0000000000..bfda06948d --- /dev/null +++ b/backend/.sqlx/query-fa62a55dd0a635995390ad4189b847b91388b6699f16afb57d8147d7e9712220.json @@ -0,0 +1,337 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE v2_job_status SET\n flow_status = JSONB_SET(\n JSONB_SET(v2_job_status.flow_status, $2::TEXT[], $3),\n $4::TEXT[], $5\n ) - $6::TEXT[],\n flow_leaf_jobs = CASE\n WHEN $7::TEXT IS NULL\n OR COALESCE(v2_job.flow_innermost_root_job, v2_job_status.id) <> v2_job_status.id\n THEN v2_job_status.flow_leaf_jobs\n ELSE JSONB_SET(COALESCE(v2_job_status.flow_leaf_jobs, '{}'::JSONB), ARRAY[$7::TEXT], $8) END\n FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id\n LEFT JOIN job_perms ON job_perms.job_id = v2_job_queue.id\n WHERE v2_job_status.id = $1 AND v2_job_queue.id = $1\n RETURNING\n v2_job_queue.workspace_id,\n v2_job_queue.id,\n v2_job.args as \"args: sqlx::types::Json>>\",\n v2_job.parent_job,\n v2_job.created_by,\n v2_job_queue.started_at,\n v2_job_queue.runnable_settings_handle,\n v2_job_queue.scheduled_for,\n v2_job.runnable_path,\n v2_job.kind as \"kind: JobKind\",\n v2_job.runnable_id as \"runnable_id: ScriptHash\",\n v2_job_queue.canceled_reason,\n v2_job_queue.canceled_by,\n v2_job.permissioned_as,\n v2_job.permissioned_as_email,\n v2_job_status.flow_status as \"flow_status: sqlx::types::Json>\",\n v2_job.tag,\n v2_job.script_lang as \"script_lang: ScriptLang\",\n v2_job.same_worker,\n v2_job.pre_run_error,\n v2_job.concurrent_limit,\n v2_job.concurrency_time_window_s,\n v2_job.flow_innermost_root_job,\n v2_job.root_job,\n v2_job.timeout,\n v2_job.flow_step_id,\n v2_job.cache_ttl,\n v2_job_queue.cache_ignore_s3_path,\n v2_job_queue.priority,\n v2_job.preprocessed,\n v2_job.script_entrypoint_override,\n v2_job.trigger,\n v2_job.trigger_kind as \"trigger_kind: TriggerKindLabel\",\n v2_job.visible_to_owner,\n NULL as permissioned_as_end_user_email,\n NULL::boolean as viewer_data_access,\n job_perms.job_token_scopes", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "args: sqlx::types::Json>>", + "type_info": "Jsonb" + }, + { + "ordinal": 3, + "name": "parent_job", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "created_by", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "started_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "runnable_settings_handle", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "scheduled_for", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "runnable_path", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "kind: JobKind", + "type_info": { + "Custom": { + "name": "job_kind", + "kind": { + "Enum": [ + "script", + "preview", + "flow", + "dependencies", + "flowpreview", + "script_hub", + "identity", + "flowdependencies", + "http", + "graphql", + "postgresql", + "noop", + "appdependencies", + "deploymentcallback", + "singlestepflow", + "flowscript", + "flownode", + "appscript", + "aiagent", + "unassigned_script", + "unassigned_flow", + "unassigned_singlestepflow" + ] + } + } + } + }, + { + "ordinal": 10, + "name": "runnable_id: ScriptHash", + "type_info": "Int8" + }, + { + "ordinal": 11, + "name": "canceled_reason", + "type_info": "Text" + }, + { + "ordinal": 12, + "name": "canceled_by", + "type_info": "Varchar" + }, + { + "ordinal": 13, + "name": "permissioned_as", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "permissioned_as_email", + "type_info": "Varchar" + }, + { + "ordinal": 15, + "name": "flow_status: sqlx::types::Json>", + "type_info": "Jsonb" + }, + { + "ordinal": 16, + "name": "tag", + "type_info": "Varchar" + }, + { + "ordinal": 17, + "name": "script_lang: ScriptLang", + "type_info": { + "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", + "rlang", + "dbt" + ] + } + } + } + }, + { + "ordinal": 18, + "name": "same_worker", + "type_info": "Bool" + }, + { + "ordinal": 19, + "name": "pre_run_error", + "type_info": "Text" + }, + { + "ordinal": 20, + "name": "concurrent_limit", + "type_info": "Int4" + }, + { + "ordinal": 21, + "name": "concurrency_time_window_s", + "type_info": "Int4" + }, + { + "ordinal": 22, + "name": "flow_innermost_root_job", + "type_info": "Uuid" + }, + { + "ordinal": 23, + "name": "root_job", + "type_info": "Uuid" + }, + { + "ordinal": 24, + "name": "timeout", + "type_info": "Int4" + }, + { + "ordinal": 25, + "name": "flow_step_id", + "type_info": "Varchar" + }, + { + "ordinal": 26, + "name": "cache_ttl", + "type_info": "Int4" + }, + { + "ordinal": 27, + "name": "cache_ignore_s3_path", + "type_info": "Bool" + }, + { + "ordinal": 28, + "name": "priority", + "type_info": "Int2" + }, + { + "ordinal": 29, + "name": "preprocessed", + "type_info": "Bool" + }, + { + "ordinal": 30, + "name": "script_entrypoint_override", + "type_info": "Varchar" + }, + { + "ordinal": 31, + "name": "trigger", + "type_info": "Varchar" + }, + { + "ordinal": 32, + "name": "trigger_kind: TriggerKindLabel", + "type_info": { + "Custom": { + "name": "job_trigger_kind", + "kind": { + "Enum": [ + "webhook", + "http", + "websocket", + "kafka", + "email", + "nats", + "schedule", + "app", + "ui", + "postgres", + "sqs", + "gcp", + "mqtt", + "nextcloud", + "google", + "ci_test", + "github", + "azure", + "asset", + "freshness", + "amqp" + ] + } + } + } + }, + { + "ordinal": 33, + "name": "visible_to_owner", + "type_info": "Bool" + }, + { + "ordinal": 34, + "name": "permissioned_as_end_user_email", + "type_info": "Text" + }, + { + "ordinal": 35, + "name": "viewer_data_access", + "type_info": "Bool" + }, + { + "ordinal": 36, + "name": "job_token_scopes", + "type_info": "TextArray" + } + ], + "parameters": { + "Left": [ + "Uuid", + "TextArray", + "Jsonb", + "TextArray", + "Jsonb", + "TextArray", + "Text", + "Jsonb" + ] + }, + "nullable": [ + false, + false, + true, + true, + false, + true, + true, + false, + true, + false, + true, + true, + true, + false, + false, + true, + false, + true, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false, + null, + null, + true + ] + }, + "hash": "fa62a55dd0a635995390ad4189b847b91388b6699f16afb57d8147d7e9712220" +} diff --git a/backend/.sqlx/query-fd04f65015069792f23edbfce320af6e69ee32b27a8f0a020e251eb2c79e43cd.json b/backend/.sqlx/query-fd04f65015069792f23edbfce320af6e69ee32b27a8f0a020e251eb2c79e43cd.json new file mode 100644 index 0000000000..4801a9b5da --- /dev/null +++ b/backend/.sqlx/query-fd04f65015069792f23edbfce320af6e69ee32b27a8f0a020e251eb2c79e43cd.json @@ -0,0 +1,71 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT email, username, is_admin, is_operator, groups, folders, end_user_email,\n job_token_scopes, viewer_data_access\n FROM job_perms WHERE job_id = $1 AND workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "username", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "is_admin", + "type_info": "Bool" + }, + { + "ordinal": 3, + "name": "is_operator", + "type_info": "Bool" + }, + { + "ordinal": 4, + "name": "groups", + "type_info": "TextArray" + }, + { + "ordinal": 5, + "name": "folders", + "type_info": "JsonbArray" + }, + { + "ordinal": 6, + "name": "end_user_email", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "job_token_scopes", + "type_info": "TextArray" + }, + { + "ordinal": 8, + "name": "viewer_data_access", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + true, + false + ] + }, + "hash": "fd04f65015069792f23edbfce320af6e69ee32b27a8f0a020e251eb2c79e43cd" +} diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 1bd38513a7..a5af71b50e 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -864,7 +864,7 @@ checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -903,7 +903,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -957,10 +957,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f9ef7a68684657ce9da341c934c73b63df7a3585a61e366d84c84ed5d3d167" dependencies = [ "attribute-derive-macro", - "manyhow 0.14.0", + "manyhow 0.14.1", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -971,12 +971,12 @@ checksum = "300d426e5e73549125bb8e4649bdd5508f82c94ddf898fb0517246e71dfb8e98" dependencies = [ "collection_literals", "interpolator", - "manyhow 0.14.0", + "manyhow 0.14.1", "proc-macro-utils", "proc-macro2", "quote", "quote-use", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -1029,11 +1029,11 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.18.1" +version = "1.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" +checksum = "faac5829c2b74c28f830747e7818ccfb684261b5f48a1118b1e2a13d36dfab13" dependencies = [ - "aws-lc-sys 0.45.0", + "aws-lc-sys 0.46.0", "untrusted 0.7.1", "zeroize", ] @@ -1052,9 +1052,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" +checksum = "f1622d8446a2d4b2ce0c7eefc73dd43a99779028d5ee5c2dd8073a658ba8a2bc" dependencies = [ "cc", "cmake", @@ -1336,9 +1336,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.12" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cba48474f1d6807384d06fec085b909f5807e16653c5af5c45dfe89539f0b70" +checksum = "f02e407fb3b54891734224b9ffac8a71fdd35f542500fa1af95754a6b2beb316" dependencies = [ "futures-util", "pin-project-lite", @@ -1421,7 +1421,7 @@ dependencies = [ "pin-project-lite", "rustls 0.21.12", "rustls 0.23.40", - "rustls-native-certs 0.8.4", + "rustls-native-certs 0.8.5", "rustls-pki-types", "tokio", "tokio-rustls 0.26.6", @@ -2010,7 +2010,7 @@ dependencies = [ "prettyplease 0.3.0", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -2034,7 +2034,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -2145,7 +2145,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c719d56f7e96194cfc53460976d3ba51c85719747c9c62ed99981847b551152b" dependencies = [ "rust_decimal", - "schemars 1.2.2", + "schemars 1.2.3", "serde", "utf8-width", ] @@ -2189,7 +2189,7 @@ checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -2366,9 +2366,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" dependencies = [ "find-msvc-tools", "jobserver", @@ -2521,7 +2521,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -2793,6 +2793,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + [[package]] name = "core_maths" version = "0.1.1" @@ -3214,7 +3220,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -3269,7 +3275,7 @@ checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ "darling_core 0.24.1", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -4575,9 +4581,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" dependencies = [ "serde_core", ] @@ -4766,7 +4772,7 @@ dependencies = [ "diplomat_core", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -4786,7 +4792,7 @@ dependencies = [ "serde", "smallvec", "strck", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -4880,7 +4886,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -5181,7 +5187,7 @@ checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -5679,7 +5685,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -5889,7 +5895,7 @@ checksum = "28f0daa179069133df60523e0f59d0a6bd2d3db9ccae01b630896d269c4a802e" dependencies = [ "attribute-derive", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -6735,7 +6741,7 @@ dependencies = [ "hyper-util", "log", "rustls 0.23.40", - "rustls-native-certs 0.8.4", + "rustls-native-certs 0.8.5", "tokio", "tokio-rustls 0.26.6", "tower-service", @@ -7801,9 +7807,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.29" +version = "1.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" +checksum = "f710a23e6dbf193214fd46ca56a9d6864e550abe86202184532ae7275e46de19" dependencies = [ "cc", "libc", @@ -8030,14 +8036,14 @@ dependencies = [ [[package]] name = "manyhow" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c69ea6e469ca785c846b25dd69eac9e9c916b51d8ada1f212866787ee1da12d" +checksum = "f1704b073316272a6785b70599f91f73b49022c2e290b065591ee6b9109ed8c7" dependencies = [ - "manyhow-macros 0.14.0", + "manyhow-macros 0.14.1", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -8053,9 +8059,9 @@ dependencies = [ [[package]] name = "manyhow-macros" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fc5c1b2c4000056dae3619fea36e79ce79dabfee4907d2dcad6612b189e370" +checksum = "31601db71f1f5fb6f831a18aa3052d13a62528ee4f2cba7e8c9a5e258d953725" dependencies = [ "proc-macro-utils", "proc-macro2", @@ -9972,7 +9978,7 @@ dependencies = [ "md-5 0.11.0", "memchr", "rand 0.10.3", - "sha2 0.11.0", + "sha2 0.11.1", "stringprep", ] @@ -10041,7 +10047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" dependencies = [ "proc-macro2", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -10160,14 +10166,15 @@ dependencies = [ [[package]] name = "proptest" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +checksum = "b8530004ccb15eae51c7e40009fbe317f341f804db54dc033eec1c50be28cfa0" dependencies = [ "bitflags 2.13.2", + "chacha20", + "core_detect", "num-traits", - "rand 0.9.5", - "rand_chacha 0.9.0", + "rand 0.10.3", "rand_xorshift", "regex-syntax 0.8.11", "unarray", @@ -10417,7 +10424,7 @@ dependencies = [ "proc-macro-utils", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -10576,11 +10583,11 @@ dependencies = [ [[package]] name = "rand_xorshift" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +checksum = "60aa6af80be32871323012e02e6e65f8a7cc7890931ae421d217ad8fe0df2ccf" dependencies = [ - "rand_core 0.9.5", + "rand_core 0.10.1", ] [[package]] @@ -10773,7 +10780,7 @@ checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -10863,7 +10870,7 @@ dependencies = [ "pin-project-lite", "quinn", "rustls 0.23.40", - "rustls-native-certs 0.8.4", + "rustls-native-certs 0.8.5", "rustls-pki-types", "serde", "serde_json", @@ -11078,7 +11085,7 @@ dependencies = [ "rand 0.10.3", "reqwest 0.13.5", "rmcp-macros", - "schemars 1.2.2", + "schemars 1.2.3", "serde", "serde_json", "sse-stream", @@ -11102,7 +11109,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -11498,9 +11505,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" dependencies = [ "openssl-probe 0.2.1", "rustls-pki-types", @@ -11548,7 +11555,7 @@ dependencies = [ "log", "once_cell", "rustls 0.23.40", - "rustls-native-certs 0.8.4", + "rustls-native-certs 0.8.5", "rustls-platform-verifier-android", "rustls-webpki 0.103.15", "security-framework 3.7.0", @@ -11735,14 +11742,14 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e" dependencies = [ "chrono", "dyn-clone", "ref-cast", - "schemars_derive 1.2.2", + "schemars_derive 1.2.3", "serde", "serde_json", ] @@ -11761,14 +11768,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" +checksum = "e1c3a92094fa7d61aa124645844facb6b554dfc797136d0f5fd1f890e2bffc69" dependencies = [ "proc-macro2", "quote", "serde_derive_internals 0.30.0", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -11966,7 +11973,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -11988,15 +11995,16 @@ checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ + "foldhash 0.2.0", "indexmap 2.14.2", "itoa", "memchr", @@ -12042,7 +12050,7 @@ checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -12094,7 +12102,7 @@ dependencies = [ "indexmap 2.14.2", "jiff", "schemars 0.9.0", - "schemars 1.2.2", + "schemars 1.2.3", "serde_core", "serde_json", "serde_with_macros", @@ -12110,7 +12118,7 @@ dependencies = [ "darling 0.24.1", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -12223,9 +12231,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" dependencies = [ "cfg-if", "cpufeatures 0.3.1", @@ -12414,9 +12422,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.16.2" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" dependencies = [ "serde", ] @@ -13345,9 +13353,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.6" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" dependencies = [ "proc-macro2", "quote", @@ -13405,7 +13413,7 @@ checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -13809,7 +13817,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -14070,7 +14078,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -14261,7 +14269,7 @@ dependencies = [ "httparse", "rand 0.8.5", "ring 0.17.14", - "rustls-native-certs 0.8.4", + "rustls-native-certs 0.8.5", "rustls-pki-types", "tokio", "tokio-rustls 0.26.6", @@ -14383,7 +14391,7 @@ dependencies = [ "percent-encoding", "pin-project", "prost 0.13.5", - "rustls-native-certs 0.8.4", + "rustls-native-certs 0.8.5", "socket2 0.5.10", "tokio", "tokio-rustls 0.26.6", @@ -14664,9 +14672,9 @@ dependencies = [ [[package]] name = "triomphe" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b40688ea6389c8171614b25491f71d4a27946e0c7ce2da1c6de27e25abf1a0ae" +checksum = "0d15f4f5898bcfc5d8a8e45ed30b2f108d710c1a2e5489dbf80e078f707181bc" dependencies = [ "serde", "stable_deref_trait", @@ -14774,7 +14782,7 @@ checksum = "f153acc4e99a5f2a5aefa09fb078be54e26271b2813f6041200b224c098d8328" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -14862,9 +14870,9 @@ checksum = "0825d2e09dcae814a4273c1e1435979d414ede25b2ee235b1545da6682483765" [[package]] name = "unicode-ident" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" [[package]] name = "unicode-normalization" @@ -15043,9 +15051,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.27.0" +version = "1.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -15224,7 +15232,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "wasm-bindgen-shared", ] @@ -15267,7 +15275,7 @@ checksum = "3b84b5ac638bfb168196a1a461fcc8f46a294a18b1b6be52133b4e0db122cc9f" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -15478,7 +15486,7 @@ dependencies = [ [[package]] name = "windmill" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-nats", @@ -15568,7 +15576,7 @@ dependencies = [ [[package]] name = "windmill-ai" -version = "1.830.0" +version = "1.833.0" dependencies = [ "async-stream", "async-trait", @@ -15603,7 +15611,7 @@ dependencies = [ [[package]] name = "windmill-alerting" -version = "1.830.0" +version = "1.833.0" dependencies = [ "axum", "chrono", @@ -15616,7 +15624,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "argon2", @@ -15759,7 +15767,7 @@ dependencies = [ [[package]] name = "windmill-api-agent-workers" -version = "1.830.0" +version = "1.833.0" dependencies = [ "axum", "chrono", @@ -15782,7 +15790,7 @@ dependencies = [ [[package]] name = "windmill-api-assets" -version = "1.830.0" +version = "1.833.0" dependencies = [ "axum", "chrono", @@ -15799,7 +15807,7 @@ dependencies = [ [[package]] name = "windmill-api-auth" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "axum", @@ -15825,7 +15833,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.830.0" +version = "1.833.0" dependencies = [ "reqwest 0.12.28", "serde", @@ -15835,7 +15843,7 @@ dependencies = [ [[package]] name = "windmill-api-configs" -version = "1.830.0" +version = "1.833.0" dependencies = [ "axum", "chrono", @@ -15852,7 +15860,7 @@ dependencies = [ [[package]] name = "windmill-api-debug" -version = "1.830.0" +version = "1.833.0" dependencies = [ "axum", "base64 0.22.1", @@ -15874,7 +15882,7 @@ dependencies = [ [[package]] name = "windmill-api-embeddings" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "axum", @@ -15897,7 +15905,7 @@ dependencies = [ [[package]] name = "windmill-api-flow-conversations" -version = "1.830.0" +version = "1.833.0" dependencies = [ "axum", "chrono", @@ -15913,7 +15921,7 @@ dependencies = [ [[package]] name = "windmill-api-flows" -version = "1.830.0" +version = "1.833.0" dependencies = [ "axum", "chrono", @@ -15935,7 +15943,7 @@ dependencies = [ [[package]] name = "windmill-api-groups" -version = "1.830.0" +version = "1.833.0" dependencies = [ "axum", "chrono", @@ -15957,7 +15965,7 @@ dependencies = [ [[package]] name = "windmill-api-inputs" -version = "1.830.0" +version = "1.833.0" dependencies = [ "axum", "chrono", @@ -15971,7 +15979,7 @@ dependencies = [ [[package]] name = "windmill-api-integration-tests" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-nats", @@ -16007,7 +16015,7 @@ dependencies = [ [[package]] name = "windmill-api-jobs" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "axum", @@ -16033,7 +16041,7 @@ dependencies = [ [[package]] name = "windmill-api-npm-proxy" -version = "1.830.0" +version = "1.833.0" dependencies = [ "axum", "chrono", @@ -16061,7 +16069,7 @@ dependencies = [ [[package]] name = "windmill-api-openapi" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "axum", @@ -16083,7 +16091,7 @@ dependencies = [ [[package]] name = "windmill-api-schedule" -version = "1.830.0" +version = "1.833.0" dependencies = [ "axum", "chrono", @@ -16103,7 +16111,7 @@ dependencies = [ [[package]] name = "windmill-api-scripts" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "axum", @@ -16145,7 +16153,7 @@ dependencies = [ [[package]] name = "windmill-api-settings" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "axum", @@ -16174,7 +16182,7 @@ dependencies = [ [[package]] name = "windmill-api-sse" -version = "1.830.0" +version = "1.833.0" dependencies = [ "lazy_static", "serde", @@ -16186,7 +16194,7 @@ dependencies = [ [[package]] name = "windmill-api-users" -version = "1.830.0" +version = "1.833.0" dependencies = [ "argon2", "axum", @@ -16210,7 +16218,7 @@ dependencies = [ [[package]] name = "windmill-api-workers" -version = "1.830.0" +version = "1.833.0" dependencies = [ "axum", "chrono", @@ -16224,7 +16232,7 @@ dependencies = [ [[package]] name = "windmill-api-workspaces" -version = "1.830.0" +version = "1.833.0" dependencies = [ "axum", "chrono", @@ -16263,7 +16271,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.830.0" +version = "1.833.0" dependencies = [ "chrono", "lazy_static", @@ -16278,7 +16286,7 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "axum", @@ -16297,7 +16305,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.830.0" +version = "1.833.0" dependencies = [ "aes-gcm", "aho-corasick", @@ -16368,7 +16376,7 @@ dependencies = [ "reqwest-retry", "rsa", "rustls 0.23.40", - "rustls-native-certs 0.8.4", + "rustls-native-certs 0.8.5", "ryu-js", "schemars 0.8.22", "semver", @@ -16413,7 +16421,7 @@ dependencies = [ [[package]] name = "windmill-dep-map" -version = "1.830.0" +version = "1.833.0" dependencies = [ "chrono", "futures", @@ -16433,7 +16441,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.830.0" +version = "1.833.0" dependencies = [ "regex", "serde", @@ -16450,7 +16458,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "astral-tokio-tar", @@ -16477,7 +16485,7 @@ dependencies = [ [[package]] name = "windmill-jseval" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "futures", @@ -16494,7 +16502,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.830.0" +version = "1.833.0" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -16510,7 +16518,7 @@ dependencies = [ [[package]] name = "windmill-mcp" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-trait", @@ -16523,6 +16531,7 @@ dependencies = [ "serde", "serde_json", "sqlx", + "sse-stream", "tokio", "tokio-util", "tracing", @@ -16531,7 +16540,7 @@ dependencies = [ [[package]] name = "windmill-native-triggers" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-trait", @@ -16562,7 +16571,7 @@ dependencies = [ [[package]] name = "windmill-oauth" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "arc-swap", @@ -16587,7 +16596,7 @@ dependencies = [ [[package]] name = "windmill-object-store" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-stream", @@ -16623,7 +16632,7 @@ dependencies = [ [[package]] name = "windmill-operator" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "futures", @@ -16642,7 +16651,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.830.0" +version = "1.833.0" dependencies = [ "convert_case 0.6.0", "serde", @@ -16651,7 +16660,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "lazy_static", @@ -16663,7 +16672,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "serde_json", @@ -16675,7 +16684,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "gosyn", @@ -16687,7 +16696,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "lazy_static", @@ -16699,7 +16708,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "serde_json", @@ -16711,7 +16720,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "nu-parser", @@ -16722,7 +16731,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -16733,7 +16742,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -16745,7 +16754,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "ruff_python_ast", @@ -16756,7 +16765,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-recursion", @@ -16779,7 +16788,7 @@ dependencies = [ [[package]] name = "windmill-parser-r" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "serde_json", @@ -16791,7 +16800,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "lazy_static", @@ -16805,7 +16814,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -16822,7 +16831,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "lazy_static", @@ -16835,7 +16844,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "serde", @@ -16847,7 +16856,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "lazy_static", @@ -16865,7 +16874,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "serde-wasm-bindgen", @@ -16881,7 +16890,7 @@ dependencies = [ [[package]] name = "windmill-parser-wac" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "ruff_python_ast", @@ -16898,7 +16907,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "lazy_static", @@ -16912,7 +16921,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-recursion", @@ -16951,7 +16960,7 @@ dependencies = [ [[package]] name = "windmill-runtime-nativets" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "const_format", @@ -16989,7 +16998,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.830.0" +version = "1.833.0" dependencies = [ "getrandom 0.3.4", "wasm-bindgen", @@ -17000,7 +17009,7 @@ dependencies = [ [[package]] name = "windmill-store" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-recursion", @@ -17035,7 +17044,7 @@ dependencies = [ [[package]] name = "windmill-test-utils" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-trait", @@ -17059,7 +17068,7 @@ dependencies = [ [[package]] name = "windmill-trigger" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-trait", @@ -17092,7 +17101,7 @@ dependencies = [ [[package]] name = "windmill-trigger-amqp" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-trait", @@ -17119,7 +17128,7 @@ dependencies = [ [[package]] name = "windmill-trigger-azure" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-trait", @@ -17152,7 +17161,7 @@ dependencies = [ [[package]] name = "windmill-trigger-email" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-trait", @@ -17172,7 +17181,7 @@ dependencies = [ [[package]] name = "windmill-trigger-gcp" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-trait", @@ -17206,7 +17215,7 @@ dependencies = [ [[package]] name = "windmill-trigger-http" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-trait", @@ -17243,7 +17252,7 @@ dependencies = [ [[package]] name = "windmill-trigger-kafka" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-trait", @@ -17266,7 +17275,7 @@ dependencies = [ [[package]] name = "windmill-trigger-mqtt" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-trait", @@ -17290,7 +17299,7 @@ dependencies = [ [[package]] name = "windmill-trigger-nats" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-nats", @@ -17314,7 +17323,7 @@ dependencies = [ [[package]] name = "windmill-trigger-postgres" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-trait", @@ -17349,7 +17358,7 @@ dependencies = [ [[package]] name = "windmill-trigger-sqs" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-trait", @@ -17377,7 +17386,7 @@ dependencies = [ [[package]] name = "windmill-trigger-websocket" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-trait", @@ -17402,7 +17411,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "bitflags 2.13.2", @@ -17421,7 +17430,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-once-cell", @@ -17483,7 +17492,7 @@ dependencies = [ "rsa", "rust_decimal", "rustls 0.23.40", - "rustls-native-certs 0.8.4", + "rustls-native-certs 0.8.5", "rustls-pemfile 2.2.0", "serde", "serde_json", @@ -17540,7 +17549,7 @@ dependencies = [ [[package]] name = "windmill-worker-volumes" -version = "1.830.0" +version = "1.833.0" dependencies = [ "bytes", "futures", @@ -18083,9 +18092,9 @@ dependencies = [ [[package]] name = "winresource" -version = "0.1.31" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" +checksum = "a2bc6d36480b829272a5bdabf08e6dca8004761c1da7e35efef800540c66221b" dependencies = [ "version_check", ] @@ -18260,24 +18269,24 @@ checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "synstructure 0.14.0", ] [[package]] name = "zerocopy" -version = "0.8.62" +version = "0.8.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.62" +version = "0.8.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" dependencies = [ "proc-macro2", "quote", @@ -18301,7 +18310,7 @@ checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "synstructure 0.14.0", ] @@ -18357,7 +18366,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index aaf5a2e8df..5cfd7372cc 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.830.0" +version = "1.833.0" authors.workspace = true edition.workspace = true @@ -88,7 +88,7 @@ members = [ exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"] [workspace.package] -version = "1.830.0" +version = "1.833.0" authors = ["Ruben Fiszel "] edition = "2021" diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 2086919735..eb8eefed27 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -ac721c18c37bb0fd41aae17e8b4439c09168511f +8979bc9f0e8a1154c66b121d290a4f571a848fb1 diff --git a/backend/migrations/20261013165527_job_perms_viewer_data_access.down.sql b/backend/migrations/20261013165527_job_perms_viewer_data_access.down.sql new file mode 100644 index 0000000000..6741203ade --- /dev/null +++ b/backend/migrations/20261013165527_job_perms_viewer_data_access.down.sql @@ -0,0 +1 @@ +ALTER TABLE job_perms DROP COLUMN IF EXISTS viewer_data_access; diff --git a/backend/migrations/20261013165527_job_perms_viewer_data_access.up.sql b/backend/migrations/20261013165527_job_perms_viewer_data_access.up.sql new file mode 100644 index 0000000000..5d6a41ece4 --- /dev/null +++ b/backend/migrations/20261013165527_job_perms_viewer_data_access.up.sql @@ -0,0 +1,4 @@ +-- Set by the server on a job pushed for an app runnable whose policy says it reads data as its +-- viewer: data table connections that name no role and object storage permission rules then go by +-- end_user_email instead of the job's own identity. +ALTER TABLE job_perms ADD COLUMN IF NOT EXISTS viewer_data_access BOOLEAN NOT NULL DEFAULT false; diff --git a/backend/oauth_connect.json b/backend/oauth_connect.json index 3c4de3b082..a29aecb92d 100644 --- a/backend/oauth_connect.json +++ b/backend/oauth_connect.json @@ -445,5 +445,90 @@ "mailboxes.read" ], "req_body_auth": true + }, + "ms_outlook": { + "req_body_auth": true, + "scopes": ["offline_access", "User.Read", "Mail.ReadWrite", "Mail.Send", "Calendars.ReadWrite", "Contacts.ReadWrite"], + "scope_options": [ + { "value": "offline_access", "label": "Stay connected (refresh token)" }, + { "value": "User.Read", "label": "Read your profile" }, + { "value": "Mail.Send", "label": "Send mail" }, + { "value": "Mail.Read", "label": "Read and search mail" }, + { "value": "Mail.ReadWrite", "label": "Read, draft, move and modify mail" }, + { "value": "MailboxSettings.Read", "label": "Read mailbox settings and categories" }, + { "value": "Contacts.Read", "label": "Read contacts" }, + { "value": "Contacts.ReadWrite", "label": "Read, create and edit contacts" }, + { "value": "People.Read", "label": "Search relevant people" }, + { "value": "Calendars.Read", "label": "Read calendars and events" }, + { "value": "Calendars.ReadWrite", "label": "Read, create, edit and delete events" }, + { "value": "Calendars.Read.Shared", "label": "Read shared calendars and find meeting times" }, + { "value": "Mail.Send.Shared", "label": "Send mail from shared mailboxes" }, + { "value": "Mail.Read.Shared", "label": "Read mail in shared mailboxes" }, + { "value": "Mail.ReadWrite.Shared", "label": "Read and modify mail in shared mailboxes" } + ], + "connect_config_template": { + "display_name": "Outlook", + "label": "Microsoft Entra tenant", + "placeholder": "common, organizations, or your tenant id", + "help_url": "https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols#endpoints", + "auth_url": "https://login.microsoftonline.com/{instance}/oauth2/v2.0/authorize", + "token_url": "https://login.microsoftonline.com/{instance}/oauth2/v2.0/token", + "req_body_auth": true, + "scopes": ["offline_access", "User.Read", "Mail.ReadWrite", "Mail.Send", "Calendars.ReadWrite", "Contacts.ReadWrite"] + } + }, + "hubspot": { + "auth_url": "https://app.hubspot.com/oauth/authorize", + "token_url": "https://api.hubapi.com/oauth/v1/token", + "scopes": [ + "oauth", + "crm.objects.contacts.read", + "crm.objects.contacts.write", + "crm.objects.companies.read", + "crm.objects.companies.write", + "crm.objects.deals.read", + "crm.objects.deals.write", + "crm.objects.owners.read" + ], + "scope_options": [ + { "value": "oauth", "label": "Basic OAuth access (always required by HubSpot)" }, + { "value": "crm.objects.contacts.read", "label": "Read contacts, and notes, tasks, meetings and calls" }, + { "value": "crm.objects.contacts.write", "label": "Create and edit contacts, and notes, tasks, meetings and calls" }, + { "value": "crm.objects.companies.read", "label": "Read companies" }, + { "value": "crm.objects.companies.write", "label": "Create and edit companies" }, + { "value": "crm.objects.deals.read", "label": "Read deals and deal pipelines" }, + { "value": "crm.objects.deals.write", "label": "Create and edit deals" }, + { "value": "crm.objects.owners.read", "label": "Read owners (users assignable to records)" }, + { "value": "tickets", "label": "Read and edit tickets and ticket pipelines" }, + { "value": "crm.objects.line_items.read", "label": "Read line items" }, + { "value": "crm.objects.line_items.write", "label": "Create and edit line items" }, + { "value": "e-commerce", "label": "Read and edit products and line items" }, + { "value": "crm.objects.quotes.read", "label": "Read quotes" }, + { "value": "crm.objects.quotes.write", "label": "Create and edit quotes" }, + { "value": "crm.objects.leads.read", "label": "Read leads" }, + { "value": "crm.objects.leads.write", "label": "Create and edit leads" }, + { "value": "crm.objects.custom.read", "label": "Read custom object records" }, + { "value": "crm.objects.custom.write", "label": "Create and edit custom object records" }, + { "value": "crm.schemas.custom.read", "label": "Read custom object schemas" }, + { "value": "crm.lists.read", "label": "Read lists (segments)" }, + { "value": "crm.lists.write", "label": "Edit lists and their members" }, + { "value": "sales-email-read", "label": "Read the content of logged one-to-one emails" }, + { "value": "forms", "label": "Read and edit forms and their submissions" }, + { "value": "files", "label": "Read and upload files in the file manager" }, + { "value": "content", "label": "Read and edit marketing emails, blog posts, pages and templates" }, + { "value": "crm.objects.marketing_events.read", "label": "Read marketing events" }, + { "value": "marketing.campaigns.read", "label": "Read marketing campaigns (Marketing Hub Professional)" }, + { "value": "communication_preferences.read_write", "label": "Read and change email subscription preferences" }, + { "value": "automation", "label": "Read, create and enroll into workflows (Professional)" }, + { "value": "automation.sequences.read", "label": "Read sequences and sequence enrollments (Sales Hub Professional)" }, + { "value": "automation.sequences.enrollments.write", "label": "Enroll contacts in sequences (Sales Hub Professional)" }, + { "value": "conversations.read", "label": "Read inboxes, threads and messages" }, + { "value": "conversations.write", "label": "Send messages and update threads" }, + { "value": "scheduler.meetings.meeting-link.read", "label": "Read meeting links and their availability" } + ], + "extra_params": { + "optional_scope": "tickets crm.objects.line_items.read crm.objects.line_items.write e-commerce crm.objects.quotes.read crm.objects.quotes.write crm.objects.leads.read crm.objects.leads.write crm.objects.custom.read crm.objects.custom.write crm.schemas.custom.read crm.lists.read crm.lists.write sales-email-read forms files content crm.objects.marketing_events.read marketing.campaigns.read communication_preferences.read_write automation automation.sequences.read automation.sequences.enrollments.write conversations.read conversations.write scheduler.meetings.meeting-link.read" + }, + "req_body_auth": true } } diff --git a/backend/parsers/windmill-parser-wasm/Cargo.lock b/backend/parsers/windmill-parser-wasm/Cargo.lock index 51878c6dfb..c0dcab1c78 100644 --- a/backend/parsers/windmill-parser-wasm/Cargo.lock +++ b/backend/parsers/windmill-parser-wasm/Cargo.lock @@ -6340,7 +6340,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windmill-common" -version = "1.830.0" +version = "1.833.0" dependencies = [ "aes-gcm", "aho-corasick", @@ -6430,7 +6430,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.830.0" +version = "1.833.0" dependencies = [ "proc-macro2", "quote", @@ -6442,7 +6442,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.830.0" +version = "1.833.0" dependencies = [ "convert_case", "serde", @@ -6451,7 +6451,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "lazy_static", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "serde_json", @@ -6475,7 +6475,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "gosyn", @@ -6487,7 +6487,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "lazy_static", @@ -6499,7 +6499,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "serde_json", @@ -6511,7 +6511,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "nu-parser", @@ -6522,7 +6522,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -6533,7 +6533,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -6545,7 +6545,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "ruff_python_ast", @@ -6556,7 +6556,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "async-recursion", @@ -6579,7 +6579,7 @@ dependencies = [ [[package]] name = "windmill-parser-r" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "serde_json", @@ -6591,7 +6591,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "lazy_static", @@ -6605,7 +6605,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "convert_case", @@ -6622,7 +6622,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "lazy_static", @@ -6635,7 +6635,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "serde", @@ -6647,7 +6647,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "lazy_static", @@ -6665,7 +6665,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "serde-wasm-bindgen", @@ -6681,7 +6681,7 @@ dependencies = [ [[package]] name = "windmill-parser-wac" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "ruff_python_ast", @@ -6698,7 +6698,7 @@ dependencies = [ [[package]] name = "windmill-parser-wasm" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "getrandom 0.2.17", @@ -6730,7 +6730,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "lazy_static", @@ -6744,7 +6744,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.830.0" +version = "1.833.0" dependencies = [ "anyhow", "bitflags", diff --git a/backend/parsers/windmill-parser-wasm/Cargo.toml b/backend/parsers/windmill-parser-wasm/Cargo.toml index 8aebf20597..ca10c14ff6 100644 --- a/backend/parsers/windmill-parser-wasm/Cargo.toml +++ b/backend/parsers/windmill-parser-wasm/Cargo.toml @@ -12,7 +12,7 @@ resolver = "2" members = ["."] [workspace.package] -version = "1.830.0" +version = "1.833.0" edition = "2021" authors = ["Ruben Fiszel "] diff --git a/backend/src/db_connect.rs b/backend/src/db_connect.rs index d4dee19f82..ab12ae6f6e 100644 --- a/backend/src/db_connect.rs +++ b/backend/src/db_connect.rs @@ -153,6 +153,9 @@ pub async fn connect( pool_options = pool_options.idle_timeout(Duration::from_secs(60)); } pool_options + .before_acquire(|_, meta| { + Box::pin(async move { Ok(!windmill_common::db::connection_is_retired(meta.age)) }) + }) .after_connect(move |conn, _| { if worker_mode { Box::pin(async move { diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index 23ab2eaed1..4fae6f34ab 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -4469,12 +4469,14 @@ pub async fn monitor_db( // Re-encrypt the legacy-format secret variables once new values are written in AES-256-GCM. // Every ~5 min until one pass has gone through every workspace; spawned so a large instance - // does not hold up the join below. + // does not hold up the join below. The column widening it starts with is tried every + // iteration: a v2 value too long for the narrow column fails to save until it is done. let legacy_secret_upgrade_f = async { if server_mode && iteration.is_some() - && iteration.as_ref().unwrap().should_run(30) - && windmill_common::variables::legacy_secret_upgrade_pending() + && (windmill_common::variables::variable_value_widen_pending() + || (iteration.as_ref().unwrap().should_run(30) + && windmill_common::variables::legacy_secret_upgrade_pending())) { if let Some(db) = conn.as_sql() { let db = db.clone(); diff --git a/backend/summarized_schema.txt b/backend/summarized_schema.txt index ac07dc06d6..fa1c049746 100644 --- a/backend/summarized_schema.txt +++ b/backend/summarized_schema.txt @@ -132,7 +132,7 @@ input: id(uuid), workspace_id(char), runnable_id(char), runnable_type(runnable_t FK: (workspace_id) -> workspace(id) instance_group: name(char), summary(char), id(char), scim_display_name(char), external_id(char) job_logs: job_id(uuid), workspace_id(char), created_at(ts), logs(text), log_offset(int), log_file_index(text[]) -job_perms: job_id(uuid), email(char), username(char), is_admin(bool), is_operator(bool), created_at(ts), workspace_id(char), groups(text[]), folders(jsonb[]), end_user_email(char), job_token_scopes(text[]) +job_perms: job_id(uuid), email(char), username(char), is_admin(bool), is_operator(bool), created_at(ts), workspace_id(char), groups(text[]), folders(jsonb[]), end_user_email(char), job_token_scopes(text[]), viewer_data_access(bool) job_resolution: job_id(uuid), workspace_id(char), resolved_at(ts), resolved_by(char), note(text), automatic(bool) job_result_stream: job_id(uuid), workspace_id(text), stream(text) job_result_stream_v2: job_id(uuid), workspace_id(text), stream(text), idx(int) diff --git a/backend/tests/ai_sessions.rs b/backend/tests/ai_sessions.rs index 6b33a4141e..b2b8406db3 100644 --- a/backend/tests/ai_sessions.rs +++ b/backend/tests/ai_sessions.rs @@ -1580,3 +1580,163 @@ async fn test_backups_fall_back_to_the_instance_storage(db: Pool) -> a Ok(()) } + +/// Runs its closure when dropped, so process-wide state is put back even when an assertion +/// fails. +struct OnDrop(F); +impl Drop for OnDrop { + fn drop(&mut self) { + (self.0)() + } +} + +/// Once every worker reads AES-256-GCM and this server has been up long enough for any server +/// of the previous release to be gone, objects are written in it: a session backed up as CBC +/// before the switch still restores whole with the GCM pieces added after it, a head at its +/// cap and a push token still read back under the larger ciphertext, and relocated GCM +/// ciphertext does not decrypt for another user. +#[sqlx::test(fixtures("base"))] +async fn test_backups_switch_to_gcm_once_every_worker_reads_it( + db: Pool, +) -> anyhow::Result<()> { + use windmill_common::min_version::{ + MIN_VERSION, MIN_VERSION_SUPPORTS_AEAD_SECRETS, MIN_VERSION_SUPPORTS_JOB_TOKEN_SCOPES, + }; + use windmill_common::workspace_crypt::V2_MAGIC; + + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let base = format!( + "http://localhost:{}/api/w/test-workspace", + server.addr.port() + ); + let storage_dir = tempfile::tempdir()?; + configure_primary_lfs(&db, &storage_dir.path().to_string_lossy()).await?; + let before = MIN_VERSION.load_full(); + let _restore = OnDrop(move || MIN_VERSION.store(before.clone())); + let _uptime = OnDrop(|| windmill_common::db::set_process_uptime_offset(Default::default())); + let is_gcm = |b: &[u8]| b.starts_with(&V2_MAGIC); + let object = |path: &str| -> Vec { + std::fs::read(user_root(storage_dir.path(), "test@windmill.dev").join(path)).unwrap() + }; + + MIN_VERSION.store(std::sync::Arc::new( + MIN_VERSION_SUPPORTS_JOB_TOKEN_SCOPES.version().clone(), + )); + let resp = push( + &base, + "SECRET_TOKEN", + json!({ + "owner": "test@windmill.dev", + "sessions": [{ + "id": "s1", + "whole": true, + "head": { "id": "s1", "workspace_id": "test-workspace", "createdAt": 1, "chatId": "c1" }, + "chats": [{ "id": "c1", "record": { "id": "c1", "title": "BEFORE" } }] + }] + }), + ) + .await?; + assert_eq!(resp.status(), 200, "{}", resp.text().await?); + let cbc_chat = object("sessions/s1/chats/c1.json"); + assert!(!cbc_chat.is_empty() && !is_gcm(&cbc_chat)); + + MIN_VERSION.store(std::sync::Arc::new( + MIN_VERSION_SUPPORTS_AEAD_SECRETS.version().clone(), + )); + // Every worker reads it, but this server was only just started: still CBC. + let resp = push( + &base, + "SECRET_TOKEN", + json!({ + "owner": "test@windmill.dev", + "sessions": [{ "id": "s1", "chats": [{ "id": "c0", "record": { "id": "c0", "title": "FRESH" } }] }] + }), + ) + .await?; + assert_eq!(resp.status(), 200, "{}", resp.text().await?); + let fresh_chat = object("sessions/s1/chats/c0.json"); + assert!(!fresh_chat.is_empty() && !is_gcm(&fresh_chat)); + + windmill_common::db::set_process_uptime_offset( + windmill_common::db::PREVIOUS_RELEASE_ROLLOUT_UPTIME, + ); + let resp = push( + &base, + "SECRET_TOKEN", + json!({ + "owner": "test@windmill.dev", + "sessions": [{ + "id": "s1", + "chats": [{ "id": "c2", "record": { "id": "c2", "title": "AFTER" } }], + "images": [{ "chat_id": "c2", "id": "i1", "data_url": "data:image/png;base64,AAAA" }] + }] + }), + ) + .await?; + assert_eq!(resp.status(), 200, "{}", resp.text().await?); + assert_eq!( + object("sessions/s1/chats/c1.json"), + cbc_chat, + "not rewritten" + ); + assert!(is_gcm(&object("sessions/s1/chats/c2.json"))); + assert!(is_gcm(&object("images/s1/c2/i1"))); + + let pulled = pull(&base, "SECRET_TOKEN", &["s1"]).await?; + let mut titles: Vec = pulled["sessions"][0]["chats"] + .as_array() + .unwrap() + .iter() + .map(|c| c["record"]["title"].as_str().unwrap().to_string()) + .collect(); + titles.sort(); + assert_eq!(titles, ["AFTER", "BEFORE", "FRESH"], "{pulled}"); + assert_eq!( + pulled["sessions"][0]["images"][0]["data_url"], + "data:image/png;base64,AAAA" + ); + + // A head at its cap, pushed in two parts: the second reads back the push token. + let mut head = json!({ "id": "s2", "workspace_id": "test-workspace", "createdAt": 2, "chatId": "c", "pad": "" }); + let pad = 1024 * 1024 - head.to_string().len(); + head["pad"] = json!("x".repeat(pad)); + assert_eq!(head.to_string().len(), 1024 * 1024); + for part in [ + json!({ "id": "s2", "whole": true, "push": "p2", "opens": true, "partial": true, "head": head }), + json!({ "id": "s2", "whole": true, "push": "p2", "chats": [{ "id": "c", "record": { "id": "c" } }] }), + ] { + let resp = push( + &base, + "SECRET_TOKEN", + json!({ "owner": "test@windmill.dev", "sessions": [part] }), + ) + .await?; + assert_eq!(resp.status(), 200, "{}", resp.text().await?); + let pushed: Value = resp.json().await?; + assert_eq!(pushed["results"], json!([{ "id": "s2" }]), "{pushed}"); + } + assert!(is_gcm(&object("sessions/s2/head.json"))); + let pulled = pull(&base, "SECRET_TOKEN", &["s2"]).await?; + assert_eq!( + pulled["sessions"][0]["head"]["pad"].as_str().map(str::len), + Some(pad) + ); + assert_eq!(pulled["sessions"][0]["chats"][0]["id"], "c", "{pulled}"); + + let first = user_root(storage_dir.path(), "test@windmill.dev"); + copy_dir( + &first, + &first + .parent() + .unwrap() + .join(calculate_hash("test2@windmill.dev")), + )?; + assert_eq!( + pull(&base, "SECRET_TOKEN_2", &["s1", "s2"]).await?["sessions"], + json!([]), + "relocated ciphertext must not decrypt for another user" + ); + + Ok(()) +} diff --git a/backend/tests/app_publisher_permit.rs b/backend/tests/app_publisher_permit.rs new file mode 100644 index 0000000000..e590efaeb2 --- /dev/null +++ b/backend/tests/app_publisher_permit.rs @@ -0,0 +1,40 @@ +//! A refused publisher-mode app run is not remembered: once the caller is granted +//! read access to the app, the very next run in the same process is allowed. + +use serde_json::json; +use sqlx::{Pool, Postgres}; +use windmill_test_utils::*; + +#[sqlx::test(fixtures("base", "app_publisher_permit"))] +async fn test_publisher_app_refusal_is_not_cached(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + let url = format!( + "http://localhost:{port}/api/w/test-workspace/apps_u/execute_component/u/test-user/papp" + ); + + let run = || async { + let resp = reqwest::Client::new() + .post(&url) + .header("Authorization", "Bearer SECRET_TOKEN_2") + .json(&json!({ "component": "c", "path": "script/u/test-user/hello", "args": {} })) + .send() + .await?; + let status = resp.status().as_u16(); + anyhow::Ok((status, resp.text().await?)) + }; + + let (status, body) = run().await?; + assert_eq!(status, 401, "unshared app must be refused: {body}"); + assert!(body.contains("Missing read permissions"), "{body}"); + + sqlx::query("UPDATE app SET extra_perms = '{\"u/test-user-2\": false}' WHERE id = 999101") + .execute(&db) + .await?; + let (status, body) = run().await?; + assert_eq!(status, 200, "a grant must apply to the next run: {body}"); + + Ok(()) +} diff --git a/backend/tests/end_user_email.rs b/backend/tests/end_user_email.rs index 0d8ff7be9a..15eb9a64f1 100644 --- a/backend/tests/end_user_email.rs +++ b/backend/tests/end_user_email.rs @@ -509,3 +509,89 @@ async fn test_raw_app_wm_end_user_email(db: Pool) -> anyhow::Result<() Ok(()) } + +/// Connecting to data tables as the viewer is honored on inline runnables only: a path runnable +/// is someone else's script, which must not read with the viewer's roles because the app's +/// policy entry for it says so. +#[sqlx::test(fixtures("base", "end_user_email"))] +async fn test_app_viewer_data_access_is_stamped_on_inline_runnables_only( + db: Pool, +) -> anyhow::Result<()> { + initialize_tracing().await; + set_jwt_secret().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + let app_path = "f/test/viewer_data_access_app"; + let script_path = "script/f/test/get_end_user_email"; + + let resp = authed( + client().post(format!( + "http://localhost:{port}/api/w/test-workspace/apps/create" + )), + SAME_WS_TOKEN, + ) + .json(&json!({ + "path": app_path, + "summary": "", + "value": { "type": "app", "grid": [], "subgrids": {}, "hiddenInlineScripts": [] }, + "policy": { + "execution_mode": "anonymous", + "on_behalf_of": null, + "on_behalf_of_email": null, + "triggerables_v2": { + "rawscript/894d2f85ea2411e777e7d5787366c9fec4c1a7a5f83482748f1d129196c9f54f": { + "static_inputs": {}, "one_of_inputs": {}, "viewer_data_access": true + }, + (script_path): { + "static_inputs": {}, "one_of_inputs": {}, "viewer_data_access": true + } + } + } + })) + .send() + .await?; + anyhow::ensure!(resp.status().is_success(), "create app: {}", resp.text().await?); + + let execute = |payload: serde_json::Value| async move { + let resp = authed( + client().post(format!( + "http://localhost:{port}/api/w/test-workspace/apps_u/execute_component/{app_path}" + )), + SAME_WS_TOKEN, + ) + .json(&payload) + .send() + .await?; + anyhow::ensure!(resp.status().is_success(), "execute: {}", resp.text().await?); + Ok::<_, anyhow::Error>(resp.text().await?.trim_matches('"').parse::()?) + }; + let as_viewer = |job_id: uuid::Uuid| { + let db = db.clone(); + async move { + sqlx::query_scalar::<_, bool>( + "SELECT viewer_data_access FROM job_perms WHERE job_id = $1", + ) + .bind(job_id) + .fetch_one(&db) + .await + } + }; + + let inline = execute(json!({ + "args": {}, + "component": "get_email", + "raw_code": { + "language": "bun", + "content": "export function main() { return process.env.WM_END_USER_EMAIL || \"\"; }" + } + })) + .await?; + assert!(as_viewer(inline).await?); + + let path = execute(json!({ "args": {}, "component": "run_script", "path": script_path })).await?; + assert!( + !as_viewer(path).await?, + "a path runnable connected as the viewer because its policy entry said so" + ); + Ok(()) +} diff --git a/backend/tests/fixtures/app_publisher_permit.sql b/backend/tests/fixtures/app_publisher_permit.sql new file mode 100644 index 0000000000..c169e53ec5 --- /dev/null +++ b/backend/tests/fixtures/app_publisher_permit.sql @@ -0,0 +1,10 @@ +-- A publisher-mode app owned by `test-user`, not shared with anyone, whose one +-- runnable is a deployed script. + +INSERT INTO script (workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES + ('test-workspace', 'test-user', 'export async function main() { return "ok" }', '{}', '', '', 'u/test-user/hello', 7788101, 'deno', ''); + +INSERT INTO app (id, workspace_id, path, summary, policy, versions) VALUES + (999101, 'test-workspace', 'u/test-user/papp', 'publisher app', + '{"execution_mode": "publisher", "on_behalf_of": "u/test-user", "on_behalf_of_email": "test@windmill.dev", + "triggerables_v2": {"c:script/u/test-user/hello": {"static_inputs": {}, "one_of_inputs": {}}}}'::jsonb, '{}'); diff --git a/backend/tests/fixtures/mcp_resource_authz.sql b/backend/tests/fixtures/mcp_resource_authz.sql index ecf3f01dc3..17fcc553b9 100644 --- a/backend/tests/fixtures/mcp_resource_authz.sql +++ b/backend/tests/fixtures/mcp_resource_authz.sql @@ -23,3 +23,19 @@ VALUES ( '{}', 'test-user' ); + +-- MCP resource whose URL lives in a variable. The host only appears in the +-- variable, so an error naming it proves the reference was resolved. +INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms) +VALUES ('test-workspace', 'u/test-user/mcp_url', 'https://mcp-from-var.invalid.windmill.test/key', false, '', '{}'); + +INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by) +VALUES ( + 'test-workspace', + 'u/test-user/var_mcp', + '{"name": "var_mcp", "url": "$var:u/test-user/mcp_url"}', + 'MCP resource with its URL in a variable', + 'mcp', + '{}', + 'test-user' +); diff --git a/backend/tests/job_s3_viewer_data_access.rs b/backend/tests/job_s3_viewer_data_access.rs new file mode 100644 index 0000000000..a68b695259 --- /dev/null +++ b/backend/tests/job_s3_viewer_data_access.rs @@ -0,0 +1,112 @@ +//! A job whose `job_perms.viewer_data_access` is set reads workspace object storage as its viewer: +//! the advanced permission rules are checked against the viewer, not the job's own identity, and +//! such a job with no viewer is refused rather than checked as itself. +//! +//! Advanced S3 permissions are an enterprise feature. +#![cfg(all(feature = "enterprise", feature = "private", feature = "parquet"))] + +use serde_json::json; +use sqlx::{Pool, Postgres}; +use uuid::Uuid; +use windmill_test_utils::*; + +/// A job of `test-user-3`, run for `viewer`, reading data as that viewer or not. +async fn job_token( + db: &Pool, + viewer: Option<&str>, + viewer_data_access: bool, +) -> anyhow::Result { + let id = Uuid::new_v4(); + sqlx::query( + "INSERT INTO v2_job (id, workspace_id, created_by, permissioned_as, permissioned_as_email, + kind, script_lang, runnable_path, tag) + VALUES ($1, 'test-workspace', 'test-user-3', 'u/test-user-3', 'test3@windmill.dev', + 'script', 'deno', 'u/test-user-3/app_runnable', 'deno')", + ) + .bind(id) + .execute(db) + .await?; + sqlx::query( + "INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, + workspace_id, end_user_email, viewer_data_access) + VALUES ($1, 'test3@windmill.dev', 'test-user-3', false, false, '{}', '{}', + 'test-workspace', $2, $3)", + ) + .bind(id) + .bind(viewer) + .bind(viewer_data_access) + .execute(db) + .await?; + Ok(windmill_common::auth::create_token_for_owner( + db, + "test-workspace", + "u/test-user-3", + "ephemeral-script", + 300, + "test3@windmill.dev", + &id, + None, + None, + ) + .await?) +} + +#[sqlx::test(fixtures("base"))] +async fn a_job_reading_data_as_its_viewer_gets_the_viewers_storage_paths( + db: Pool, +) -> anyhow::Result<()> { + initialize_tracing().await; + set_jwt_secret().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + let storage_dir = tempfile::tempdir()?; + for user in ["test-user-2", "test-user-3"] { + let dir = storage_dir.path().join("u").join(user); + std::fs::create_dir_all(&dir)?; + std::fs::write(dir.join("file.txt"), user)?; + } + sqlx::query("UPDATE workspace_settings SET large_file_storage = $1 WHERE workspace_id = $2") + .bind(json!({ + "type": "FilesystemStorage", + "root_path": storage_dir.path().to_string_lossy(), + "public_resource": null, + "advanced_permissions": [{ "pattern": "u/{username}/**", "allow": "read" }] + })) + .bind("test-workspace") + .execute(&db) + .await?; + + let read = |token: String, user: &'static str| async move { + let resp = reqwest::Client::new() + .get(format!( + "http://localhost:{port}/api/w/test-workspace/job_helpers/download_s3_file" + )) + .query(&[("file_key", format!("u/{user}/file.txt"))]) + .bearer_auth(token) + .send() + .await?; + Ok::<_, anyhow::Error>(resp.status().is_success()) + }; + + let own = job_token(&db, Some("test2@windmill.dev"), false).await?; + assert!(read(own.clone(), "test-user-3").await?); + assert!( + !read(own, "test-user-2").await?, + "a job not reading as its viewer reached the viewer's paths" + ); + + let as_viewer = job_token(&db, Some("test2@windmill.dev"), true).await?; + assert!(read(as_viewer.clone(), "test-user-2").await?); + assert!( + !read(as_viewer, "test-user-3").await?, + "a job reading as its viewer kept its own identity's paths" + ); + + let no_viewer = job_token(&db, None, true).await?; + assert!( + !read(no_viewer, "test-user-3").await?, + "a job reading as a viewer it does not have was checked as itself" + ); + Ok(()) +} diff --git a/backend/tests/mcp_resource_authz.rs b/backend/tests/mcp_resource_authz.rs index 0fa35933f4..6ccf5f4f2d 100644 --- a/backend/tests/mcp_resource_authz.rs +++ b/backend/tests/mcp_resource_authz.rs @@ -23,9 +23,9 @@ use windmill_common::client::AuthedClient; use windmill_test_utils::*; use windmill_worker::{load_mcp_tools, McpResourceConfig}; -fn config() -> Vec { +fn config(resource_path: &str) -> Vec { vec![McpResourceConfig { - resource_path: "$res:f/private/admin_mcp".to_string(), + resource_path: resource_path.to_string(), include_tools: None, exclude_tools: None, }] @@ -34,7 +34,16 @@ fn config() -> Vec { // The Ok variant ((HashMap<_, Arc>, Vec)) does not implement // Debug, so `expect_err` is unavailable — extract the error explicitly. async fn load_err(db: &Pool, client: &AuthedClient, ctx: &str) -> String { - match load_mcp_tools(db, "test-workspace", config(), client).await { + load_err_for(db, client, "$res:f/private/admin_mcp", ctx).await +} + +async fn load_err_for( + db: &Pool, + client: &AuthedClient, + resource_path: &str, + ctx: &str, +) -> String { + match load_mcp_tools(db, "test-workspace", config(resource_path), client).await { Ok(_) => panic!("{ctx}"), Err(e) => e.to_string(), } @@ -100,3 +109,31 @@ async fn test_mcp_resource_not_loaded_without_authorization( Ok(()) } + +/// A `$var:` in the resource's `url` is resolved before connecting, rather than +/// being handed to the client as a literal (and rejected as a relative URL). +#[sqlx::test(fixtures("base", "mcp_resource_authz"))] +async fn test_mcp_resource_url_variable_is_resolved(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + + let server = ApiServer::start(db.clone()).await?; + let client = AuthedClient::new( + format!("http://localhost:{}", server.addr.port()), + "test-workspace".to_string(), + "SECRET_TOKEN".to_string(), + None, + ); + let msg = load_err_for( + &db, + &client, + "$res:u/test-user/var_mcp", + "the variable's URL is non-resolvable, so the connect step must fail", + ) + .await; + assert!( + msg.contains("mcp-from-var.invalid.windmill.test"), + "the URL should come from the variable: {msg}" + ); + + Ok(()) +} diff --git a/backend/tests/s3_proxy_headers.rs b/backend/tests/s3_proxy_headers.rs new file mode 100644 index 0000000000..8f43f8c761 --- /dev/null +++ b/backend/tests/s3_proxy_headers.rs @@ -0,0 +1,29 @@ +//! Every s3_proxy response carries `nosniff` and the sandbox CSP, error responses included, +//! since the presigned route serves workspace objects from the app origin without a login. +#![cfg(all(feature = "enterprise", feature = "private", feature = "parquet"))] + +use sqlx::{Pool, Postgres}; +use windmill_test_utils::*; + +#[sqlx::test(fixtures("base"))] +async fn test_s3_proxy_sets_content_security_headers(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + let resp = reqwest::Client::new() + .get(format!( + "http://localhost:{port}/api/w/test-workspace/s3_proxy/_default_/x.html?exp=9999999999&sig=00" + )) + .send() + .await?; + + assert!(resp.status().is_client_error(), "status: {}", resp.status()); + assert_eq!(resp.headers()["x-content-type-options"], "nosniff"); + assert_eq!( + resp.headers()["content-security-policy"], + "sandbox allow-scripts allow-forms allow-popups allow-modals allow-downloads" + ); + Ok(()) +} diff --git a/backend/windmill-api-embeddings/src/lib.rs b/backend/windmill-api-embeddings/src/lib.rs index 6e4d6a1c71..a470648cbb 100644 --- a/backend/windmill-api-embeddings/src/lib.rs +++ b/backend/windmill-api-embeddings/src/lib.rs @@ -9,16 +9,11 @@ use windmill_common::DEFAULT_HUB_BASE_URL; #[cfg(feature = "embedding")] use windmill_common::HUB_BASE_URL; -use axum::Router; - -#[cfg(feature = "embedding")] use axum::{ extract::{Path, Query}, - Json, + routing::get, + Extension, Json, Router, }; - -#[cfg(feature = "embedding")] -use axum::routing::get; #[cfg(feature = "embedding")] use candle_core::{Device, Tensor}; #[cfg(feature = "embedding")] @@ -27,10 +22,7 @@ use candle_nn::VarBuilder; use candle_transformers::models::bert::{BertModel, Config, DTYPE}; #[cfg(feature = "embedding")] use hf_hub::api::tokio::Api; -#[cfg(feature = "embedding")] -use serde::Deserialize; -#[cfg(feature = "embedding")] -use serde::Serialize; +use serde::{Deserialize, Serialize}; #[cfg(feature = "embedding")] use sqlx::{Pool, Postgres}; #[cfg(feature = "embedding")] @@ -44,9 +36,7 @@ use tokenizers::Tokenizer; use tokio::sync::RwLock; #[cfg(feature = "embedding")] use windmill_common::utils::http_get_from_hub; - -#[cfg(feature = "embedding")] -use windmill_common::error::JsonResult; +use windmill_common::{db::DB, error::JsonResult}; #[cfg(feature = "embedding")] use windmill_store::resources::ResourceType; @@ -60,6 +50,7 @@ lazy_static::lazy_static! { // full pulling interval, so a transient startup error doesn't leave the // embeddings DB uninitialized for a whole day. pub static ref HUB_EMBEDDINGS_RETRY_INTERVAL_SECS: u64 = std::env::var("HUB_EMBEDDINGS_RETRY_INTERVAL_SECS").ok().map(|x| x.parse::().ok()).flatten().unwrap_or(60); + pub static ref RESOURCE_TYPE_EMBEDDINGS_CHECK_INTERVAL_SECS: u64 = std::env::var("RESOURCE_TYPE_EMBEDDINGS_CHECK_INTERVAL_SECS").ok().map(|x| x.parse::().ok()).flatten().unwrap_or(60); } #[cfg(feature = "embedding")] @@ -104,14 +95,12 @@ async fn query_hub_scripts( } } -#[cfg(feature = "embedding")] #[derive(Deserialize)] struct ResourceTypesQuery { text: String, limit: Option, } -#[cfg(feature = "embedding")] #[derive(Serialize)] pub struct ResourceTypeResult { name: String, @@ -138,24 +127,107 @@ fn trim_to_top_score( .take_while(|r| (top_score - score(r)) / top_score <= max_relative_drop) .collect() } -#[cfg(feature = "embedding")] async fn query_resource_types( + Extension(db): Extension, Query(query): Query, Path(w_id): Path, ) -> JsonResult> { - let embeddings_db = EMBEDDINGS_DB.read().await; - - if let Some(embeddings_db) = embeddings_db.as_ref() { - let results = embeddings_db - .query_resource_types(w_id, &query.text, query.limit) - .await?; - - Ok(Json(results)) - } else { - Err(windmill_common::error::Error::internal_err( - "Embeddings db not initialized".to_string(), - )) + #[cfg(feature = "embedding")] + if let Some(embeddings_db) = EMBEDDINGS_DB.read().await.as_ref() { + return Ok(Json( + embeddings_db + .query_resource_types(w_id, &query.text, query.limit) + .await?, + )); } + Ok(Json( + match_resource_types_by_text(&db, &w_id, &query.text, query.limit).await?, + )) +} + +/// Serves the search when there is no embeddings index: a build without the `embedding` +/// feature, or one whose index never loaded (air-gapped, `DISABLE_EMBEDDING`). The AI chat +/// has no other way to read the instance's resource types. +async fn match_resource_types_by_text( + db: &DB, + w_id: &str, + text: &str, + limit: Option, +) -> windmill_common::error::Result> { + let words: Vec = text + .to_lowercase() + .split(|c: char| !c.is_alphanumeric()) + .filter(|word| !word.is_empty()) + .map(str::to_string) + .collect(); + let rows: Vec<(String, String, Option)> = sqlx::query_as( + "SELECT workspace_id, name, description FROM resource_type WHERE workspace_id = $1 OR workspace_id = 'admins'", + ) + .bind(w_id) + .fetch_all(db) + .await?; + + let mut ranked: Vec<(f32, String, String)> = rows + .into_iter() + .map(|(workspace_id, name, description)| { + let score = text_match_score(&words, &name, description.as_deref().unwrap_or_default()); + (score, workspace_id, name) + }) + .filter(|(score, _, _)| *score > 0.0) + .collect(); + ranked.sort_by(|a, b| b.0.total_cmp(&a.0).then_with(|| a.2.cmp(&b.2))); + ranked.truncate(limit.unwrap_or(10).max(0) as usize); + + // Schemas are read for the kept rows only: a synced instance holds megabytes of them. + let (workspaces, names): (Vec<&str>, Vec<&str>) = ranked + .iter() + .map(|(_, workspace_id, name)| (workspace_id.as_str(), name.as_str())) + .unzip(); + let mut schemas: Vec<(String, String, Option)> = sqlx::query_as( + "SELECT workspace_id, name, schema FROM resource_type WHERE (workspace_id, name) IN (SELECT * FROM unnest($1::text[], $2::text[]))", + ) + .bind(&workspaces) + .bind(&names) + .fetch_all(db) + .await?; + + Ok(ranked + .into_iter() + .map(|(score, workspace_id, name)| { + let schema = schemas + .iter_mut() + .find(|(w, n, _)| *w == workspace_id && *n == name) + .and_then(|(_, _, schema)| schema.take()); + ResourceTypeResult { score, name, schema } + }) + .collect()) +} + +/// A word found in the name counts twice a word found in the description: names are what +/// a query for a service spells out ("postgres" in `postgresql`, "sheets" in `gsheets`). +/// A word under 3 characters ("s3", "ai") would sit inside too many names, so it counts only +/// as a whole name or one of its `_`-separated parts. +fn text_match_score(words: &[String], name: &str, description: &str) -> f32 { + let name = name.to_lowercase(); + let description = description.to_lowercase(); + words + .iter() + .map(|word| { + let short = word.len() < 3; + let in_name = if short { + name.split('_').any(|part| part == word) + } else { + name.contains(word.as_str()) + }; + if in_name { + 2.0 + } else if !short && description.contains(word.as_str()) { + 1.0 + } else { + 0.0 + } + }) + .sum() } #[cfg(feature = "embedding")] @@ -288,17 +360,60 @@ impl ModelInstance { pub struct EmbeddingsDb { db: Db, model_instance: Arc, + /// Kept from the last full fill, so the resource types can be re-indexed between fills + /// without downloading the hub's embeddings again. + hub_resource_type_embeddings: Arc>>, + /// The `resource_types_fingerprint` the resource types collection was built from. + resource_types_fingerprint: Option, + /// See `ResourceTypeIndex::custom_vectors`. + custom_resource_type_vectors: Arc>>, +} + +/// A built resource types collection, with what a later refresh needs to tell it is stale and +/// to reuse its vectors. +#[cfg(feature = "embedding")] +struct ResourceTypeIndex { + fingerprint: Option, + embeddings: Vec, + /// Vectors of the types the hub has none for, by the `name;description` they embed: a + /// refresh runs the model only on types that are new or changed, not on every custom type. + custom_vectors: Arc>>, } #[cfg(feature = "embedding")] impl EmbeddingsDb { + fn empty(model_instance: Arc) -> Self { + Self { + db: Db::new(), + model_instance, + hub_resource_type_embeddings: Arc::new(HashMap::new()), + resource_types_fingerprint: None, + custom_resource_type_vectors: Arc::new(HashMap::new()), + } + } + pub async fn new(pg_db: &Pool, model_instance: Arc) -> Result { - let db = Db::new(); - - let mut embeddings_db = Self { db, model_instance: model_instance.clone() }; - + let mut embeddings_db = Self::empty(model_instance); embeddings_db.fill_db(pg_db).await?; + Ok(embeddings_db) + } + /// The resource types alone, all embedded locally, for when the full fill cannot download + /// the hub's embeddings: an air-gapped instance, or a private hub serving none. Hub script + /// search keeps failing, since it has no collection to search. + async fn resource_types_only( + pg_db: &Pool, + model_instance: Arc, + ) -> Result { + let mut embeddings_db = Self::empty(model_instance); + let index = resource_type_index( + pg_db, + &embeddings_db.model_instance, + &embeddings_db.hub_resource_type_embeddings, + &embeddings_db.custom_resource_type_vectors, + ) + .await?; + embeddings_db.replace_resource_types(index)?; Ok(embeddings_db) } @@ -310,13 +425,6 @@ impl EmbeddingsDb { self.db .create_collection("scripts".to_string(), 384, Distance::Cosine)?; - if self.db.get_collection("resource_types").is_some() { - self.db.delete_collection("resource_types")?; - } - - self.db - .create_collection("resource_types".to_string(), 384, Distance::Cosine)?; - let hub_base_url = (**HUB_BASE_URL.load()).clone(); let response = match hub_base_url.as_str() { @@ -425,44 +533,40 @@ impl EmbeddingsDb { )); } let hub_resource_types = response.json::>().await?; + self.hub_resource_type_embeddings = Arc::new( + hub_resource_types + .into_iter() + .map(|rt| (rt.name, rt.embedding)) + .collect(), + ); - let resource_types: Vec = - sqlx::query_as!(ResourceType, "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset, display_name from resource_type ORDER BY name",) - .fetch_all(pg_db) - .await?; + let index = resource_type_index( + pg_db, + &self.model_instance, + &self.hub_resource_type_embeddings, + &self.custom_resource_type_vectors, + ) + .await?; + self.replace_resource_types(index) + } - for rt in resource_types { - let mut hm = HashMap::new(); - hm.insert("name".to_string(), rt.name.clone()); - if let Some(schema) = rt.schema.clone() { - hm.insert("schema".to_string(), serde_json::to_string(&schema)?); - } - hm.insert("workspace".to_string(), rt.workspace_id.clone()); - let hub_rt = hub_resource_types.iter().find(|hrt| hrt.name == rt.name); - - let vector = if let Some(hub_rt) = hub_rt { - hub_rt.embedding.clone() - } else { - self.model_instance - .clone() - .create_embedding(&format!( - "{};{}", - rt.name, - rt.description.unwrap_or_default() - )) - .await? - }; - - let embedding = Embedding { - id: format!("{}_{}", rt.workspace_id, rt.name), - vector, - metadata: Some(hm), - }; - - self.db - .insert_into_collection("resource_types", embedding)?; + fn replace_resource_types(&mut self, index: ResourceTypeIndex) -> Result<()> { + // Built aside and swapped in whole: an insert the collection refuses must not leave + // searches on a half-filled index. + const NEXT: &str = "resource_types_next"; + self.db.collections.remove(NEXT); + self.db + .create_collection(NEXT.to_string(), 384, Distance::Cosine)?; + for embedding in index.embeddings { + self.db.insert_into_collection(NEXT, embedding)?; } - + if let Some(next) = self.db.collections.remove(NEXT) { + self.db + .collections + .insert("resource_types".to_string(), next); + } + self.resource_types_fingerprint = index.fingerprint; + self.custom_resource_type_vectors = index.custom_vectors; Ok(()) } @@ -617,6 +721,101 @@ impl EmbeddingsDb { } } +/// Covers everything an indexed resource type is built from, whichever path wrote it: the +/// create and update routes, a hub sync's direct upserts, a delete. +/// +/// A sum of per-row hashes needs no sort. Don't go back to an ORDER BY aggregate: sorting +/// every schema spills to disk once they exceed work_mem, on every check of every server. +#[cfg(feature = "embedding")] +async fn resource_types_fingerprint(pg_db: &Pool) -> Result> { + Ok(sqlx::query_scalar( + "SELECT count(*)::text || ':' || coalesce(sum(('x' || left(md5(workspace_id || '/' || name || '/' || coalesce(description, '') || '/' || coalesce(schema::text, '')), 15))::bit(60)::bigint), 0)::text FROM resource_type", + ) + .fetch_one(pg_db) + .await?) +} + +/// The fingerprint is read before the rows: a write landing in between leaves it stale, so +/// the next check indexes again rather than keeping an index that misses the write. +#[cfg(feature = "embedding")] +async fn resource_type_index( + pg_db: &Pool, + model_instance: &Arc, + hub_embeddings: &HashMap>, + known_custom_vectors: &HashMap>, +) -> Result { + let fingerprint = resource_types_fingerprint(pg_db).await?; + let resource_types: Vec = + sqlx::query_as!(ResourceType, "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset, display_name from resource_type ORDER BY name",) + .fetch_all(pg_db) + .await?; + + let mut embeddings = Vec::with_capacity(resource_types.len()); + let mut custom_vectors = HashMap::new(); + for rt in resource_types { + let mut hm = HashMap::new(); + hm.insert("name".to_string(), rt.name.clone()); + if let Some(schema) = rt.schema.clone() { + hm.insert("schema".to_string(), serde_json::to_string(&schema)?); + } + hm.insert("workspace".to_string(), rt.workspace_id.clone()); + + let vector = match hub_embeddings.get(&rt.name) { + Some(vector) => vector.clone(), + None => { + let text = format!("{};{}", rt.name, rt.description.unwrap_or_default()); + let vector = match known_custom_vectors + .get(&text) + .or_else(|| custom_vectors.get(&text)) + { + Some(vector) => vector.clone(), + None => model_instance.clone().create_embedding(&text).await?, + }; + custom_vectors.insert(text, vector.clone()); + vector + } + }; + + embeddings.push(Embedding { + // Neither part can hold a `/`, so two types never share an id; with `_` between + // them, `acme_c` + `c_x` and `acme` + `c_c_x` would. + id: format!("{}/{}", rt.workspace_id, rt.name), + vector, + metadata: Some(hm), + }); + } + Ok(ResourceTypeIndex { fingerprint, embeddings, custom_vectors: Arc::new(custom_vectors) }) +} + +/// The full fill runs once a day, so a resource type created or synced in between would stay +/// unsearchable until then; this re-indexes the resource types alone when the table changed. +#[cfg(feature = "embedding")] +pub async fn refresh_resource_type_embeddings(pg_db: &Pool) -> Result<()> { + if EMBEDDINGS_DB.read().await.is_none() { + return Ok(()); + } + let fingerprint = resource_types_fingerprint(pg_db).await?; + let (model_instance, hub_embeddings, custom_vectors) = match EMBEDDINGS_DB.read().await.as_ref() + { + Some(db) if db.resource_types_fingerprint != fingerprint => ( + db.model_instance.clone(), + db.hub_resource_type_embeddings.clone(), + db.custom_resource_type_vectors.clone(), + ), + _ => return Ok(()), + }; + let index = + resource_type_index(pg_db, &model_instance, &hub_embeddings, &custom_vectors).await?; + if let Some(db) = EMBEDDINGS_DB.write().await.as_mut() { + // A full fill that landed meanwhile indexed the types from newer hub vectors than + // the ones this index was built from. + if Arc::ptr_eq(&db.hub_resource_type_embeddings, &hub_embeddings) { + db.replace_resource_types(index)?; + } + } + Ok(()) +} + #[cfg(feature = "embedding")] fn normalize_l2(v: &Tensor) -> Result { Ok(v.broadcast_div(&v.sqr()?.sum_keepdim(1)?.sqrt()?)?) @@ -630,6 +829,19 @@ pub fn load_embeddings_db(db: &Pool) -> () { .unwrap_or(false); if !disable_embedding { + let resource_types_db = db.clone(); + tokio::spawn(async move { + loop { + tokio::time::sleep(std::time::Duration::from_secs( + *RESOURCE_TYPE_EMBEDDINGS_CHECK_INTERVAL_SECS, + )) + .await; + if let Err(e) = refresh_resource_type_embeddings(&resource_types_db).await { + tracing::warn!("Failed to refresh resource type embeddings: {e:#}"); + } + } + }); + let db_clone = db.clone(); tokio::spawn(async move { // Keep retrying model init: a transient failure here must not @@ -664,6 +876,9 @@ pub fn load_embeddings_db(db: &Pool) -> () { backoff_secs = *HUB_EMBEDDINGS_RETRY_INTERVAL_SECS; *HUB_EMBEDDINGS_PULLING_INTERVAL_SECS } else { + if let Err(e) = index_resource_types_alone(&db_clone).await { + tracing::warn!("Failed to index the resource types alone: {e:#}"); + } let secs = backoff_secs; backoff_secs = backoff_secs .saturating_mul(2) @@ -696,7 +911,29 @@ pub async fn update_embeddings_db(db: &Pool) -> bool { } } +/// After a failed full fill, so a fill still downloading at startup is not raced. A full fill +/// that succeeds later replaces this index. #[cfg(feature = "embedding")] +async fn index_resource_types_alone(pg_db: &Pool) -> Result<()> { + if EMBEDDINGS_DB.read().await.is_some() { + return Ok(()); + } + let Some(model_instance) = MODEL_INSTANCE.read().await.clone() else { + return Ok(()); + }; + let started = std::time::Instant::now(); + let embeddings_db = EmbeddingsDb::resource_types_only(pg_db, model_instance).await?; + let mut current = EMBEDDINGS_DB.write().await; + if current.is_none() { + *current = Some(embeddings_db); + tracing::info!( + "Indexed the resource types alone in {:?}, without the hub's embeddings", + started.elapsed() + ); + } + Ok(()) +} + pub fn workspaced_service() -> Router { Router::new().route("/query_resource_types", get(query_resource_types)) } @@ -706,11 +943,6 @@ pub fn global_service() -> Router { Router::new().route("/query_hub_scripts", get(query_hub_scripts)) } -#[cfg(not(feature = "embedding"))] -pub fn workspaced_service() -> Router { - Router::new() -} - #[cfg(not(feature = "embedding"))] pub fn global_service() -> Router { Router::new() @@ -768,3 +1000,34 @@ mod tests { assert_eq!(trim_to_top_score(vec![0.42f32], 0.05, |s| *s), vec![0.42]); } } + +#[cfg(test)] +mod text_match_tests { + use super::text_match_score; + + // A query names the service, which a type's name only contains ("postgres" in + // `postgresql`): an exact comparison finds nothing for "postgres database". + #[test] + fn ranks_a_name_match_above_a_description_match() { + let words = vec!["postgres".to_string(), "database".to_string()]; + assert_eq!( + text_match_score(&words, "PostgreSQL", "PostgreSQL connection"), + 2.0 + ); + assert_eq!( + text_match_score(&words, "mysql", "MySQL database credentials"), + 1.0 + ); + assert_eq!(text_match_score(&words, "slack", "Slack bot token"), 0.0); + } + + // Many services go by a short name; matching it inside other names would rank `ms365` + // with `s3`. + #[test] + fn matches_a_short_word_only_as_a_whole_name_part() { + let words = vec!["s3".to_string()]; + assert_eq!(text_match_score(&words, "s3", "Amazon S3 bucket"), 2.0); + assert_eq!(text_match_score(&words, "aws_s3", ""), 2.0); + assert_eq!(text_match_score(&words, "ms365", "mentions s3"), 0.0); + } +} diff --git a/backend/windmill-api-integration-tests/tests/datatable_roles.rs b/backend/windmill-api-integration-tests/tests/datatable_roles.rs index 023d9f20ed..0acba27c41 100644 --- a/backend/windmill-api-integration-tests/tests/datatable_roles.rs +++ b/backend/windmill-api-integration-tests/tests/datatable_roles.rs @@ -1850,10 +1850,12 @@ async fn cleanup_waits_out_a_save_racing_it_for_an_instance_database( #[cfg(all(feature = "private", feature = "enterprise"))] #[sqlx::test(migrations = "../migrations", fixtures("base", "datatable_roles"))] -async fn a_connection_naming_no_role_holds_the_default_one_and_the_viewers( +async fn a_connection_naming_no_role_holds_the_default_one_and_its_identitys( db: Pool, ) -> anyhow::Result<()> { - use windmill_common::workspaces::{get_datatable_resource_from_db, DatatableAccess}; + use windmill_common::workspaces::{ + get_datatable_resource_from_db, DatatableAccess, DatatableConnectAs, + }; initialize_tracing().await; let server = ApiServer::start(db.clone()).await?; @@ -1869,20 +1871,25 @@ async fn a_connection_naming_no_role_holds_the_default_one_and_the_viewers( JOIN pg_roles r ON r.oid = am.roleid JOIN pg_roles m ON m.oid = am.member WHERE r.rolname = $1 AND m.rolname LIKE 'wm\\_set\\_%'"; - // The job is the app publisher's in every case: only the viewer the server recorded differs. - let connect = |role: Option<&'static str>, viewer: Option<&'static str>| { + let connect_job = |owner: &'static str, role: Option<&'static str>, connect_as| { + let (permissioned_as, email) = match owner { + "test-user" => ("u/test-user", "test@windmill.dev"), + "test-user-2" => ("u/test-user-2", "test2@windmill.dev"), + _ => ("u/test-user-3", "test3@windmill.dev"), + }; get_datatable_resource_from_db( &db, "test-workspace", "main", role, - DatatableAccess::PermissionedAs { - permissioned_as: "u/test-user", - email: "test@windmill.dev", - end_user_email: viewer, - }, + DatatableAccess::PermissionedAs { permissioned_as, email, connect_as }, ) }; + // An app published by the workspace admin `test-user`, run by `viewer`, whose runnable + // connects as its viewer. + let connect = |role: Option<&'static str>, viewer: Option<&'static str>| { + connect_job("test-user", role, DatatableConnectAs::Viewer(viewer)) + }; let set_permissions = |default_role: String, roles: Value| { sqlx::query( "UPDATE workspace_settings @@ -1953,11 +1960,25 @@ async fn a_connection_naming_no_role_holds_the_default_one_and_the_viewers( expected ); - // An admin may use every role, the one owning the tables included. As a viewer they add - // what names them, which here is nothing; nor does a job with no viewer recorded. - for viewer in [Some("test@windmill.dev"), None] { - assert_eq!(connect(None, viewer).await?["user"], names[0].as_str()); - } + // A runnable that does not connect as its viewer holds its own identity's roles, whoever + // views it: here the publisher, an admin, who may use every role but adds only what + // names them, which is nothing. + assert_eq!( + connect_job("test-user", None, DatatableConnectAs::Job).await?["user"], + names[0].as_str(), + "the viewer's roles reached a job that does not connect as its viewer" + ); + // A job with no viewer, a schedule say, holds its owner's roles. + assert_eq!( + connect_job("test-user-2", None, DatatableConnectAs::Job).await?, + all + ); + assert_eq!( + connect(None, Some("test@windmill.dev")).await?["user"], + names[0].as_str() + ); + // Connecting as a viewer it does not have is refused rather than made as the job. + assert!(connect(None, None).await.is_err()); // A named role is that role alone, whoever is viewing. let named: &'static str = Box::leak(names[1].clone().into_boxed_str()); assert_eq!( @@ -1978,29 +1999,28 @@ async fn a_connection_naming_no_role_holds_the_default_one_and_the_viewers( two, "no default changes nothing for a viewer the roles name" ); + assert_eq!( + connect_job("test-user-3", None, DatatableConnectAs::Job).await?, + two + ); + assert_eq!( + connect_job("test-user", None, DatatableConnectAs::Job).await?["user"], + names[0].as_str(), + "the role open to everyone names the admin too" + ); assert!(connect(None, None).await.is_err()); // The same holds for a default the job's own identity is not a tenant of. let mut closed = roles.clone(); closed[ids[0].as_str()] = json!({"tenants": []}); set_permissions(ids[0].clone(), closed).await?; - let as_member = |viewer: Option<&'static str>| { - get_datatable_resource_from_db( - &db, - "test-workspace", - "main", - None, - DatatableAccess::PermissionedAs { - permissioned_as: "u/test-user-3", - email: "test3@windmill.dev", - end_user_email: viewer, - }, - ) - }; assert_eq!( - as_member(Some("test3@windmill.dev")).await?["user"], + connect_job("test-user-3", None, DatatableConnectAs::Job).await?["user"], + names[1].as_str() + ); + assert_eq!( + connect(None, Some("test3@windmill.dev")).await?["user"], names[1].as_str() ); - assert!(as_member(None).await.is_err()); set_permissions(ids[0].clone(), roles).await?; // A set login nobody connects with any more is dropped, a recent one is kept, and a diff --git a/backend/windmill-api-integration-tests/tests/resources.rs b/backend/windmill-api-integration-tests/tests/resources.rs index 9c167ef70c..823c415818 100644 --- a/backend/windmill-api-integration-tests/tests/resources.rs +++ b/backend/windmill-api-integration-tests/tests/resources.rs @@ -751,6 +751,65 @@ async fn test_resource_cache_handles_job_context(db: Pool) -> anyhow:: Ok(()) } +/// Without an embeddings index the resource type search reads the table, so it has to apply the +/// same scope as the index: the workspace's own types and the global ones in `admins`, never +/// another workspace's. +#[sqlx::test(migrations = "../migrations", fixtures("base"))] +async fn test_resource_type_search_without_embeddings(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + sqlx::query("INSERT INTO workspace (id, name, owner) VALUES ('other-workspace', 'other-workspace', 'test-user')") + .execute(&db) + .await?; + for (workspace, name, description) in [ + ( + "test-workspace", + "c_acme_billing", + "Acme internal billing API", + ), + ( + "other-workspace", + "c_other_billing", + "Another workspace's billing API", + ), + ] { + sqlx::query( + "INSERT INTO resource_type (workspace_id, name, schema, description, created_by) VALUES ($1, $2, '{}'::jsonb, $3, 'test-user')", + ) + .bind(workspace) + .bind(name) + .bind(description) + .execute(&db) + .await?; + } + + let search = |text: &str| { + let url = format!( + "http://localhost:{port}/api/w/test-workspace/embeddings/query_resource_types?text={text}" + ); + async move { + let resp = authed(client().get(url)).send().await.unwrap(); + assert_eq!(resp.status(), 200); + resp.json::>() + .await + .unwrap() + .into_iter() + .map(|rt| rt["name"].as_str().unwrap().to_string()) + .collect::>() + } + }; + + let billing = search("billing").await; + assert_eq!(billing.first().map(String::as_str), Some("c_acme_billing")); + assert!(!billing.contains(&"c_other_billing".to_string())); + // `ai_skill` is seeded in `admins` by the migrations. + assert!(search("skill").await.contains(&"ai_skill".to_string())); + + Ok(()) +} + #[cfg(feature = "mcp")] #[sqlx::test(migrations = "../migrations", fixtures("base", "resources_test"))] async fn test_mcp_tools(db: Pool) -> anyhow::Result<()> { diff --git a/backend/windmill-api-jobs/src/execution.rs b/backend/windmill-api-jobs/src/execution.rs index c6c2a1fede..330e96c2fb 100644 --- a/backend/windmill-api-jobs/src/execution.rs +++ b/backend/windmill-api-jobs/src/execution.rs @@ -266,6 +266,22 @@ pub async fn run_end_user_email( } } +/// [`run_end_user_email`] of an inline run of the caller's own code, and whether its data table +/// connections are made as that end user: they are when the calling job's are, since the code is +/// that job's own. +pub async fn run_inline_preview_end_user( + db: &DB, + w_id: &str, + authed: &ApiAuthed, +) -> error::Result<(Option, bool)> { + match authed.job_id { + Some(job_id) => Ok(windmill_common::auth::get_job_perms(db, &job_id, w_id) + .await? + .map_or((None, false), |p| (p.end_user_email, p.viewer_data_access))), + None => Ok((Some(authed.email.clone()), false)), + } +} + #[cfg(feature = "enterprise")] pub async fn check_license_key_valid() -> error::Result<()> { use windmill_common::ee_oss::LICENSE_KEY_VALID; diff --git a/backend/windmill-api-settings/src/lib.rs b/backend/windmill-api-settings/src/lib.rs index 1359e8c142..f9ba1ff86f 100644 --- a/backend/windmill-api-settings/src/lib.rs +++ b/backend/windmill-api-settings/src/lib.rs @@ -132,6 +132,15 @@ async fn get_instance_ui( )) } +/// Whether email notifications can go out, for the workspace admins who configure email +/// handlers and cannot read `smtp_settings` itself (it holds the password). Asks the same +/// resolver the senders use, so SMTP set through `SMTP_*` env vars counts too. +async fn get_smtp_configured(Extension(db): Extension, _authed: ApiAuthed) -> JsonResult { + Ok(Json( + SMTP_ENABLED && windmill_common::server::is_smtp_configured(&db).await?, + )) +} + pub fn global_service() -> Router { #[warn(unused_mut)] let r = Router::new() @@ -152,6 +161,7 @@ pub fn global_service() -> Router { ) .route("/instance_config/yaml", get(get_instance_config_yaml)) .route("/test_smtp", post(test_email)) + .route("/smtp_configured", get(get_smtp_configured)) .route("/test_license_key", post(test_license_key)) .route("/send_stats", post(send_stats)) .route("/get_stats", get(get_stats)) diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 8b5edc12cb..989e5c2a48 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.830.0 + version: 1.833.0 title: Windmill API contact: @@ -1971,6 +1971,24 @@ paths: application/json: schema: {} + /settings/smtp_configured: + get: + summary: whether instance SMTP is configured + description: | + Readable by any authenticated user. True when this build can send email + and SMTP is set, either in the smtp_settings instance setting or through + the SMTP_* environment variables. + operationId: getSmtpConfigured + tags: + - setting + responses: + "200": + description: whether email notifications can be sent + content: + application/json: + schema: + type: boolean + /settings/test_smtp: post: summary: test smtp diff --git a/backend/windmill-api/src/ai_sessions.rs b/backend/windmill-api/src/ai_sessions.rs index 9e980a54f4..097e5c4862 100644 --- a/backend/windmill-api/src/ai_sessions.rs +++ b/backend/windmill-api/src/ai_sessions.rs @@ -24,7 +24,6 @@ use axum::{ Extension, Json, Router, }; use futures::{StreamExt, TryStreamExt}; -use magic_crypt::MagicCryptTrait; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; use std::sync::Arc; @@ -33,9 +32,11 @@ use windmill_api_workspaces::ai_session_backups::{ fallback_store, generation_prefix, primary_store, storage_id, MAX_OBJECT_BYTES, }; use windmill_api_workspaces::workspaces::sessions_retention_days; +use windmill_common::db::{process_uptime, PREVIOUS_RELEASE_ROLLOUT_UPTIME}; use windmill_common::error::{Error, JsonResult, Result}; use windmill_common::utils::calculate_hash; use windmill_common::variables::{crypt_from_key_with_suffix, get_workspace_key, WorkspaceCrypt}; +use windmill_common::workspace_crypt::{CipherFormat, MAX_BYTES_OVERHEAD}; use windmill_object_store::object_store_reexports::{ ObjectStore, ObjectStoreError, Path as ObjectPath, PutPayload, }; @@ -47,9 +48,9 @@ const PULL_BODY_LIMIT: usize = 64 * 1024; /// A pull answer larger than this hands the remaining ids back as `deferred`. const PULL_RESPONSE_BUDGET: usize = 32 * 1024 * 1024; const MAX_HEAD_BYTES: usize = 1024 * 1024; -/// What the cipher adds to a plaintext at most (a block of padding): an object stored at a -/// cap is that much larger than the cap when read back. -const CIPHER_PADDING: usize = 16; +/// What the cipher adds to a plaintext at most: an object stored at a cap is that much larger +/// than the cap when read back. +const CIPHER_PADDING: usize = MAX_BYTES_OVERHEAD; /// The browser bounds an image to a 1568 px edge and re-encodes past 700 KB; this is /// well above what that produces. const MAX_IMAGE_BYTES: usize = 4 * 1024 * 1024; @@ -120,6 +121,15 @@ pub fn workspaced_service() -> Router { ) } +fn backup_format(current: CipherFormat, uptime: std::time::Duration) -> CipherFormat { + // A previous-release server reads only CBC here: GCM waits for it to be rolled out. + if current == CipherFormat::V2 && uptime >= PREVIOUS_RELEASE_ROLLOUT_UPTIME { + CipherFormat::V2 + } else { + CipherFormat::LegacyCbc + } +} + /// What reading an object yields. `Gone`: not there (deleted since the listing, or never /// pushed). `Grown`: larger than expected, so replaced since the listing (or planted), and /// left unread; a pull answers with a page ending before it rather than without it. @@ -214,7 +224,8 @@ impl Backend { } fn seal(&self, plaintext: &[u8]) -> Vec { - self.mc.encrypt_bytes_to_bytes(plaintext) + let format = backup_format(CipherFormat::current(), process_uptime()); + self.mc.encrypt_bytes_in(plaintext, format) } /// Bytes written. @@ -255,7 +266,7 @@ impl Backend { // apart beyond the cipher's padding check, which a wrong key passes now and then. match self .mc - .decrypt_bytes_to_bytes(&bytes) + .decrypt_bytes(&bytes) .ok() .and_then(|plaintext| String::from_utf8(plaintext).ok()) { @@ -1632,3 +1643,32 @@ async fn push( results, })) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn backups_are_written_in_gcm_only_once_the_previous_release_is_rolled_out() { + let just_short = PREVIOUS_RELEASE_ROLLOUT_UPTIME - std::time::Duration::from_secs(1); + for (current, uptime, expected) in [ + ( + CipherFormat::V2, + PREVIOUS_RELEASE_ROLLOUT_UPTIME, + CipherFormat::V2, + ), + (CipherFormat::V2, just_short, CipherFormat::LegacyCbc), + ( + CipherFormat::LegacyCbc, + PREVIOUS_RELEASE_ROLLOUT_UPTIME * 4, + CipherFormat::LegacyCbc, + ), + ] { + assert_eq!( + backup_format(current, uptime), + expected, + "{current:?} {uptime:?}" + ); + } + } +} diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index 2c2179d830..4b67f865f9 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -481,6 +481,12 @@ pub struct PolicyTriggerableInputs { sensitive_inputs: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] tag: Option, + /// The runnable reads data as the app's viewer rather than as the app's identity: data table + /// connections that name no role, and object storage advanced permission rules. Honored for + /// inline runnables only: their code is deployed with the app, so it is the publisher who + /// chose to read with the viewer's rights. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + viewer_data_access: bool, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -1009,7 +1015,6 @@ fn raw_app_wrapper_html(secret: &str) -> String { -App
- + {#snippet children({ item })} @@ -418,21 +290,21 @@
- {#if handlerSelected === 'custom'} + {#if draft.selected === 'custom'}
- {#if !handlerPath} + {#if !draft.custom.path}
- {#if handlerPath} + {#if draft.custom.path}

Extra arguments

{#await import('$lib/components/SchemaForm.svelte')} @@ -465,7 +337,7 @@ @@ -475,7 +347,7 @@ {/if}
{/if} - {:else if handlerSelected === 'slack'} + {:else if draft.selected === 'slack'} { - if (e.detail && errorOrRecovery === 'error') { - handlerPath = slackHandlerScriptPath - } else if (e.detail && errorOrRecovery === 'recovery') { - handlerPath = slackRecoveryHandler - } else if (e.detail && errorOrRecovery === 'success') { - handlerPath = slackSuccessHandler - } else { - handlerPath = undefined - } + on:change={(e) => { + draft.slack.path = e.detail ? builtinHubPath(errorOrRecovery, 'slack') : undefined }} /> {/if} - {#if workspaceConnectedToSlack && isSlackHandler(handlerPath)} + {#if workspaceConnectedToSlack && draft.slack.path}
{#await import('$lib/components/SchemaForm.svelte')} @@ -515,18 +379,18 @@ schemaFieldTooltip={{ channel: 'Slack channel name without the "#" - example: "windmill-alerts"' }} - bind:args={handlerExtraArgs} + bind:args={draft.slack.args} shouldHideNoInputs className="text-xs" /> {/await} - {#if $enterpriseLicense && isSlackHandler(handlerPath)} + {#if $enterpriseLicense} {#if connectionTestJob !== undefined} @@ -559,7 +423,7 @@ {:else if workspaceConnectedToSlack == undefined} {/if} - {:else if handlerSelected === 'teams'} + {:else if draft.selected === 'teams'} { - if (e.detail && errorOrRecovery === 'error') { - handlerPath = teamsHandlerScriptPath - } else if (e.detail && errorOrRecovery === 'recovery') { - handlerPath = teamsRecoveryHandler - } else if (e.detail && errorOrRecovery === 'success') { - handlerPath = teamsSuccessHandler - } else { - handlerPath = undefined - } + on:change={(e) => { + draft.teams.path = e.detail ? builtinHubPath(errorOrRecovery, 'teams') : undefined }} /> {/if} @@ -604,26 +460,26 @@ placeholder="Search Teams channels" workspace={effectiveWorkspace} teamId={teams_team_guid} - selectedChannel={handlerExtraArgs['channel'] + selectedChannel={draft.teams.args['channel'] ? { - channel_id: handlerExtraArgs['channel'], - channel_name: handlerExtraArgs['channel_name'] + channel_id: draft.teams.args['channel'], + channel_name: draft.teams.args['channel_name'] } : undefined} onSelectedChannelChange={(channel) => { - handlerExtraArgs['channel'] = channel?.channel_id - handlerExtraArgs['channel_name'] = channel?.channel_name + draft.teams.args['channel'] = channel?.channel_id + draft.teams.args['channel_name'] = channel?.channel_name }} onError={(e) => sendUserToast('Failed to load channels: ' + e.message, true)} />
- {#if $enterpriseLicense && isTeamsHandler(handlerPath) && workspaceConnectedToTeams} + {#if $enterpriseLicense && draft.teams.path && workspaceConnectedToTeams} {#if connectionTestJob !== undefined} @@ -649,7 +505,7 @@ {:else if workspaceConnectedToTeams == undefined} {/if} - {:else if handlerSelected === 'email'} + {:else if draft.selected === 'email'} {#if isCloudHosted()} Email notifications for trigger failures are only available in self-hosted Windmill @@ -662,8 +518,8 @@ handlerExtraArgs[EMAIL_RECIPIENTS_KEY] ?? [], - (recipients) => (handlerExtraArgs[EMAIL_RECIPIENTS_KEY] = recipients) + () => draft.email.args.email_recipients ?? [], + (recipients) => (draft.email.args.email_recipients = recipients) } placeholder="Enter email addresses..." onCreateItem={(email) => { @@ -672,23 +528,24 @@ sendUserToast('Invalid email format', true) return } - const currentArray = handlerExtraArgs[EMAIL_RECIPIENTS_KEY] ?? [] - handlerExtraArgs[EMAIL_RECIPIENTS_KEY] = [...currentArray, email] + draft.email.args.email_recipients = [ + ...(draft.email.args.email_recipients ?? []), + email + ] }} class="w-full" /> - {#if handlerExtraArgs[EMAIL_RECIPIENTS_KEY]?.length > 0} + {#if draft.email.args.email_recipients?.length > 0} - {handlerExtraArgs[EMAIL_RECIPIENTS_KEY]?.length} email{handlerExtraArgs[ - EMAIL_RECIPIENTS_KEY - ]?.length === 1 + {draft.email.args.email_recipients.length} email{draft.email.args.email_recipients + .length === 1 ? '' : 's'} configured {/if}
{/if} - {:else if handlerSelected === 'instance_alerts'} + {:else if draft.selected === 'instance_alerts'}
Failed jobs are reported to the Slack, Teams and email channels configured at the instance diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index 1a468ac33a..17f984bac9 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -40,7 +40,7 @@ import { getContext, onDestroy, setContext, untrack, type Snippet } from 'svelte' import PageHeaderContent from '$lib/components/PageHeaderContent.svelte' import PathEditPopover from '$lib/components/PathEditPopover.svelte' - import { pageHeader, PHONE_BAR } from '$lib/components/pageHeaderRegistry.svelte' + import { pageHeader } from '$lib/components/pageHeaderRegistry.svelte' import { writable } from 'svelte/store' import CenteredPage from './CenteredPage.svelte' import { Button } from './common' @@ -230,19 +230,14 @@ let pathSnapshot = $state(undefined) let topbarWidth = $state(0) - // In the page header the buttons share the row with the breadcrumb, so they collapse sooner: - // the trail and the summary take ~560px before either truncates, and the full group — the - // dropdown, Diff, the preview trio and Deploy — is ~610px. - const compactBelow = $derived(ownsPageHeader ? 1200 : 720) - const compactTopbar = $derived.by(() => { - const w = ownsPageHeader ? pageHeader.barWidth : topbarWidth - return w > 0 && w < compactBelow - }) - // A phone's bar holds the trail and Deploy: the diff joins the menu and the preview buttons - // stand down, since there is no room to run a flow beside its own name. - const phoneTopbar = $derived( - ownsPageHeader && pageHeader.barWidth > 0 && pageHeader.barWidth < PHONE_BAR + // In the page header the bar folds the buttons (`actionFolds` below), once its breadcrumb has + // given all it can; anywhere else this editor's own top bar is theirs alone. + const compactTopbar = $derived( + ownsPageHeader ? pageHeader.actionFold >= 1 : topbarWidth > 0 && topbarWidth < 720 ) + // The second fold holds the trail and Deploy: the diff joins the menu and the preview buttons + // stand down, since there is no room to run a flow beside its own name. + const phoneTopbar = $derived(ownsPageHeader && pageHeader.actionFold >= 2) const diffEnabled = $derived(customUi?.topBar?.diff != false) // Nothing to compare against until a deployed version exists. @@ -1588,6 +1583,7 @@ // Frozen while the rename popover is open so the trail holds still as the user types. kind: 'flow', path: pathSnapshot ?? $pathStore, + summary: flowStore.val.summary, summaryContent: flowSummary, // Gated like the summary's own editor: a host that allows neither field keeps the // band's segment, which copies the path. @@ -1597,6 +1593,7 @@ : undefined }} actions={flowHeaderActions} + actionFolds={2} contexts={headerContexts} separator="none" /> diff --git a/frontend/src/lib/components/NavBreadcrumb.svelte b/frontend/src/lib/components/NavBreadcrumb.svelte index b053591fc0..4df66e0362 100644 --- a/frontend/src/lib/components/NavBreadcrumb.svelte +++ b/frontend/src/lib/components/NavBreadcrumb.svelte @@ -16,7 +16,6 @@ not this component) — so the lead can run the whole line. import WorkspaceItemKindIcon from './WorkspaceItemKindIcon.svelte' import { Menu, Menubar } from '$lib/components/meltComponents' import MeltButton from '$lib/components/meltComponents/MeltButton.svelte' - import BreadcrumbItemContent from './BreadcrumbItemContent.svelte' import WorkspacePickerBody from '$lib/components/sidebar/WorkspacePickerBody.svelte' import BreadcrumbSegment from '$lib/components/BreadcrumbSegment.svelte' import { userWorkspaces, workspaceStore, workspaceColor, superadmin } from '$lib/stores' @@ -33,14 +32,25 @@ not this component) — so the lead can run the whole line. import { KIND_LABEL_LOWER, kindKey } from '$lib/components/workspacePicker' import type { PageHeaderItem, PageHeaderSection } from './pageHeaderRegistry.svelte' import type { Snippet } from 'svelte' + import { + BAR_STAGES, + FLOOR, + shownPathLevels, + type Part, + type TrailMeasures, + type TrailStage + } from './pageHeaderStages' let { item, section, afterName, actingWorkspaceId, - narrow = false, - nameOnly = false + nameOnly = false, + stage = BAR_STAGES[0], + caps = {}, + scopesGive = false, + measures = $bindable() }: { item?: PageHeaderItem section?: PageHeaderSection @@ -49,9 +59,14 @@ not this component) — so the lead can run the whole line. afterName?: Snippet /** The workspace the page acts on, when it differs from the one the app is pointed at. */ actingWorkspaceId?: string - /** The bar is short of room: the path holds a tighter cap, so it cannot claim a third of the - * line before anything else has given way. The names truncate at every width either way. */ - narrow?: boolean + /** Which parts the bar has dropped for room (see pageHeaderStages). */ + stage?: TrailStage + /** Max widths the bar holds the names to, once dropping whole parts is not enough. */ + caps?: Partial> + /** The folders above the name truncate too: even the name at its last floor does not fit. */ + scopesGive?: boolean + /** Filled in for the bar to choose a stage with; left undefined, nothing is measured. */ + measures?: TrailMeasures /** Only the page's own name, with no workspace part and no picker — the band inside a * session's preview frame, where the workspace is the host's and leading the reader out of * it is exactly what that band must not offer. The page still has to say what it is. */ @@ -145,6 +160,9 @@ not this component) — so the lead can run the whole line. // The summary is the band's, not the breadcrumb's (see PageHeaderBar), so the path reads as the // last segment only for an item that has no summary to follow it. const itemIsLast = $derived(!!item && !item.summaryContent && !item.summary) + /** Only the summary's text names the item: a control offering to add one does not, and the path + * then stays at every stage. */ + const summaryNamesItem = $derived(!!item?.summary?.trim()) // No hover fill: the chevron beside it is what reacts to the pointer. The accent stays on the // fork's own name inside, so the family's name is not painted as if it were the fork. @@ -156,15 +174,8 @@ not this component) — so the lead can run the whole line. twMerge(SEGMENT, 'hover:bg-transparent', workspaceIsLast && !showFork ? LAST : LEAD) ) - // Capped: a draft's path is a 40-character uuid slug, and the trail yields so grudgingly that - // one would push a page's buttons off the end of the bar. const pathSegment = $derived( - twMerge( - SEGMENT, - 'gap-1.5 overflow-hidden', - itemIsLast ? LAST : LEAD, - narrow ? 'max-w-[11rem]' : 'max-w-[16rem]' - ) + twMerge(SEGMENT, 'gap-1.5 overflow-hidden', itemIsLast ? LAST : LEAD) ) // Copying is the one thing a segment does that leaves no mark on the page, so it is the one // without a hover fill: the pointer and the confirmation under it are the affordance. A segment @@ -191,6 +202,18 @@ not this component) — so the lead can run the whole line. const scopeKind = $derived(item?.path?.split('/')[0]) /** The path's levels, the `f`/`u` prefix dropped — that is what the icon says. */ const pathLevels = $derived((item?.path ?? '').split('/').slice(1).filter(Boolean)) + + // The off-screen copies stand in a fixed-width box for what a page renders into the trail: a + // second render of a page's snippet would be a second live copy of its controls. + let afterNameWidth = $state(0) + let sectionContentWidth = $state(0) + + /** A measured copy holds each name at its floor; the trail on screen at the bar's cap. */ + function capStyle(part: Part, measure: boolean): string | undefined { + if (measure) return `max-width: ${FLOOR[part]}px` + const cap = caps[part] + return cap === undefined ? undefined : `max-width: ${cap}px` + } {#snippet workspaceDisc()} @@ -241,89 +264,129 @@ not this component) — so the lead can run the whole line. {/snippet} -