From 1dc4ed5e7fb86c9658f1f877e7b92cd9dab5e283 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Fri, 9 Oct 2026 19:13:48 +0200 Subject: [PATCH 01/20] feat: add a jobs:run:dependencies token scope for lock generation (#11643) * feat: add a jobs:run:dependencies token scope for lock generation Co-Authored-By: Claude Opus 5.5 * test: pin lock-job reads to the dependencies scope Co-Authored-By: Claude Opus 5.5 * fix: align scope labels with and without a restrict paths button Co-Authored-By: Claude Opus 5.5 * fix: match a lock job's stored path literally on job reads Co-Authored-By: Claude Opus 5.5 * fix: drop the dependency-lock warning from the jobs scope heading Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KNiYsRRDyvkRYwkHda6uyf --------- Co-authored-by: Claude Opus 5.5 --- ...f378e9fac4986cd29f69c1f2b8a83ba515c3.json} | 4 +- backend/tests/fixtures/jobs_read_auth.sql | 26 ++++++++++ backend/tests/jobs_read_auth.rs | 38 ++++++++++++++ backend/windmill-api-auth/src/scopes.rs | 52 ++++++++++++++++--- backend/windmill-api/src/jobs.rs | 33 +++++++----- backend/windmill-api/src/token.rs | 5 ++ .../components/settings/ScopeSelector.svelte | 6 +-- 7 files changed, 139 insertions(+), 25 deletions(-) rename backend/.sqlx/{query-7904ac35748b1de2c7ff764f22787436d0fe74f99f298e5c399233f0d247f2ec.json => query-8aef4f9d27ec6df07cd671bcf90bf378e9fac4986cd29f69c1f2b8a83ba515c3.json} (54%) diff --git a/backend/.sqlx/query-7904ac35748b1de2c7ff764f22787436d0fe74f99f298e5c399233f0d247f2ec.json b/backend/.sqlx/query-8aef4f9d27ec6df07cd671bcf90bf378e9fac4986cd29f69c1f2b8a83ba515c3.json similarity index 54% rename from backend/.sqlx/query-7904ac35748b1de2c7ff764f22787436d0fe74f99f298e5c399233f0d247f2ec.json rename to backend/.sqlx/query-8aef4f9d27ec6df07cd671bcf90bf378e9fac4986cd29f69c1f2b8a83ba515c3.json index e103a91c1e..53a9daaf9a 100644 --- a/backend/.sqlx/query-7904ac35748b1de2c7ff764f22787436d0fe74f99f298e5c399233f0d247f2ec.json +++ b/backend/.sqlx/query-8aef4f9d27ec6df07cd671bcf90bf378e9fac4986cd29f69c1f2b8a83ba515c3.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "WITH RECURSIVE chain(id, parent_job) AS (\n SELECT id, parent_job FROM v2_job WHERE id = $1 AND workspace_id = $2\n UNION ALL\n SELECT j.id, j.parent_job FROM v2_job j\n JOIN chain c ON j.id = c.parent_job AND j.workspace_id = $2\n )\n SELECT\n CASE WHEN a.agent THEN regexp_replace(j.runnable_path, '\\.chat$', '')\n ELSE j.runnable_path END AS runnable_path,\n CASE\n WHEN a.agent THEN 'agents'\n WHEN j.kind IN ('script', 'script_hub', 'unassigned_script') THEN 'scripts'\n WHEN j.kind IN ('flow', 'unassigned_flow') THEN 'flows'\n WHEN j.kind IN ('singlestepflow', 'unassigned_singlestepflow') THEN\n CASE WHEN COALESCE(\n (SELECT m->'value'->>'type'\n FROM jsonb_array_elements(j.raw_flow->'modules') m\n WHERE m->>'id' IN ('a', 'main')\n LIMIT 1),\n 'script'\n ) = 'flow' THEN 'flows' ELSE 'scripts' END\n END AS scope_kind,\n CASE WHEN j.trigger_kind = 'app' THEN j.trigger END AS launched_by_app\n FROM v2_job j JOIN chain c ON c.id = j.id,\n LATERAL (SELECT j.kind = 'flowpreview'\n AND j.raw_flow->'modules'->1 IS NULL\n AND j.raw_flow->'modules'->0->>'id' = '__wm_agent_root' AS agent) a\n WHERE j.workspace_id = $2", + "query": "WITH RECURSIVE chain(id, parent_job) AS (\n SELECT id, parent_job FROM v2_job WHERE id = $1 AND workspace_id = $2\n UNION ALL\n SELECT j.id, j.parent_job FROM v2_job j\n JOIN chain c ON j.id = c.parent_job AND j.workspace_id = $2\n )\n SELECT\n CASE WHEN a.agent THEN regexp_replace(j.runnable_path, '\\.chat$', '')\n ELSE j.runnable_path END AS runnable_path,\n CASE\n WHEN a.agent THEN 'agents'\n WHEN j.kind IN ('script', 'script_hub', 'unassigned_script') THEN 'scripts'\n WHEN j.kind IN ('flow', 'unassigned_flow') THEN 'flows'\n WHEN j.kind IN ('dependencies', 'flowdependencies') THEN 'dependencies'\n WHEN j.kind IN ('singlestepflow', 'unassigned_singlestepflow') THEN\n CASE WHEN COALESCE(\n (SELECT m->'value'->>'type'\n FROM jsonb_array_elements(j.raw_flow->'modules') m\n WHERE m->>'id' IN ('a', 'main')\n LIMIT 1),\n 'script'\n ) = 'flow' THEN 'flows' ELSE 'scripts' END\n END AS scope_kind,\n CASE WHEN j.trigger_kind = 'app' THEN j.trigger END AS launched_by_app\n FROM v2_job j JOIN chain c ON c.id = j.id,\n LATERAL (SELECT j.kind = 'flowpreview'\n AND j.raw_flow->'modules'->1 IS NULL\n AND j.raw_flow->'modules'->0->>'id' = '__wm_agent_root' AS agent) a\n WHERE j.workspace_id = $2", "describe": { "columns": [ { @@ -31,5 +31,5 @@ null ] }, - "hash": "7904ac35748b1de2c7ff764f22787436d0fe74f99f298e5c399233f0d247f2ec" + "hash": "8aef4f9d27ec6df07cd671bcf90bf378e9fac4986cd29f69c1f2b8a83ba515c3" } diff --git a/backend/tests/fixtures/jobs_read_auth.sql b/backend/tests/fixtures/jobs_read_auth.sql index e32b777a74..e175a9c065 100644 --- a/backend/tests/fixtures/jobs_read_auth.sql +++ b/backend/tests/fixtures/jobs_read_auth.sql @@ -98,6 +98,32 @@ INSERT INTO public.v2_job_completed (id, workspace_id, duration_ms, status, resu ('18181818-1818-1818-1818-181818181818', 'test-workspace', 1000, 'success'::job_status, '{"agent": "AGENT_CHAT_RESULT"}'); +-- A lock job for flow `f/shared/flow1`, as `wmill generate-metadata` queues it, and the +-- token CI would lock with. +INSERT INTO token(token_hash, token_prefix, token, email, label, super_admin, scopes) VALUES ( + encode(sha256('RUN_SCOPED_DEPS_TOKEN'::bytea), 'hex'), 'RUN_DEPS_', 'RUN_SCOPED_DEPS_TOKEN', + 'test2@windmill.dev', 'lock token', false, + ARRAY['jobs:run:dependencies:f/shared/*'] +); +INSERT INTO public.v2_job ( + id, workspace_id, created_by, created_at, permissioned_as, permissioned_as_email, + kind, runnable_path, tag, visible_to_owner +) VALUES ( + '19191919-1919-1919-1919-191919191919', 'test-workspace', 'test-user-2', + '2023-01-01 00:00:00', 'u/test-user-2', 'test2@windmill.dev', + 'flowdependencies', 'f/shared/flow1', 'dependency', true +), ( + -- Queued by a broader token with `*` as its path, which the request body allows. + '20202020-2020-2020-2020-202020202020', 'test-workspace', 'test-user-2', + '2023-01-01 00:00:00', 'u/test-user-2', 'test2@windmill.dev', + 'dependencies', '*', 'dependency', true +); +INSERT INTO public.v2_job_completed (id, workspace_id, duration_ms, status, result) VALUES + ('19191919-1919-1919-1919-191919191919', 'test-workspace', 1000, 'success'::job_status, + '{"lock": "DEPS_RESULT"}'), + ('20202020-2020-2020-2020-202020202020', 'test-workspace', 1000, 'success'::job_status, + '{"lock": "WILDCARD_DEPS_RESULT"}'); + -- A token pairing an app scope with a run scope, as someone driving an app's components -- programmatically would build. `APP_INLINE_JOB` is an inline-script component run: no -- `jobs:run` scope can name its kind, so only the `apps:run` half puts it in reach. diff --git a/backend/tests/jobs_read_auth.rs b/backend/tests/jobs_read_auth.rs index 6a101200a1..bffacfbaf0 100644 --- a/backend/tests/jobs_read_auth.rs +++ b/backend/tests/jobs_read_auth.rs @@ -476,6 +476,44 @@ async fn test_single_job_read_authorization(db: Pool) -> anyhow::Resul ); } + // A lock job belongs to `jobs:run:dependencies`: its path is whatever the caller sent, + // so a token scoped to run the flow at that path must not read it. + let path = "completed/get_result/19191919-1919-1919-1919-191919191919"; + let (status, body) = get(&base, path, Some("RUN_SCOPED_DEPS_TOKEN")).await; + assert!( + status.is_success() && body.contains("DEPS_RESULT"), + "dependencies-scoped token must read its lock job (got {status}): {body}" + ); + let (status, body) = get(&base, path, Some("RUN_SCOPED_TOKEN")).await; + assert_eq!( + status, + reqwest::StatusCode::NOT_FOUND, + "a flow-scoped token must not read a lock job (got {status}): {body}" + ); + let (status, body) = get( + &base, + &format!("completed/get_result/{FLOW_JOB}"), + Some("RUN_SCOPED_DEPS_TOKEN"), + ) + .await; + assert_eq!( + status, + reqwest::StatusCode::NOT_FOUND, + "dependencies-scoped token must not read a flow run at its path (got {status}): {body}" + ); + // A lock job filed at `*` matches no path-scoped grant: the stored path is not a pattern. + let (status, body) = get( + &base, + "completed/get_result/20202020-2020-2020-2020-202020202020", + Some("RUN_SCOPED_DEPS_TOKEN"), + ) + .await; + assert_eq!( + status, + reqwest::StatusCode::NOT_FOUND, + "a path-scoped token must not read a lock job filed at `*` (got {status}): {body}" + ); + // An `apps:run:` scope is a start grant too: the inline-script component run it // launched — a kind no `jobs:run` scope can name — stays readable to a token scoped // to that app, and stays out of reach for one that is only scoped to run jobs. diff --git a/backend/windmill-api-auth/src/scopes.rs b/backend/windmill-api-auth/src/scopes.rs index d586b6ba63..21f916bf94 100644 --- a/backend/windmill-api-auth/src/scopes.rs +++ b/backend/windmill-api-auth/src/scopes.rs @@ -170,10 +170,16 @@ const FLOW_JOBS: [&'static str; 6] = [ /// scoped by the agent's path like a deployed runnable's, as `jobs:run:agents:`. const AGENT_JOBS: [&'static str; 1] = ["jobs/run/agent"]; +/// Lock resolution for scripts, apps and flows, scoped as `jobs:run:dependencies:`. +/// Resolving a package can run its build or install scripts on the worker, so this is +/// still code execution: the scope only takes away the direct ways to run code. +const DEPENDENCY_JOBS: [&'static str; 2] = ["jobs/run/dependencies", "jobs/run/flow_dependencies"]; + lazy_static::lazy_static! { static ref RUN_PATH_ACTIONS: Vec<&'static str> = { - let mut v = vec!["jobs/resume/", "jobs/run/batch_rerun_jobs", "jobs/run/workflow_as_code", "jobs/run/dependencies","jobs/run/flow_dependencies", "apps_u/execute_component", "apps_u/upload_s3_file"]; + let mut v = vec!["jobs/resume/", "jobs/run/batch_rerun_jobs", "jobs/run/workflow_as_code", "apps_u/execute_component", "apps_u/upload_s3_file"]; + v.extend(DEPENDENCY_JOBS); v.extend(SCRIPT_JOBS); v.extend(FLOW_JOBS); v.extend(AGENT_JOBS); @@ -227,11 +233,13 @@ fn map_http_method_to_action(method: &str, route_path: &str) -> ScopeAction { } } -/// Checks the route path to determine the runnable kind ("agents", "flows" or "scripts"). +/// Checks the route path to determine the runnable kind ("dependencies", "agents", "flows" +/// or "scripts"). /// /// The order of checks is important: -/// - Flow-related paths are checked first to avoid false positives, as some flow paths -/// (e.g., `/run_preview_flow`) share prefixes with script paths (e.g., `/run_preview`). +/// - Dependency paths are checked before flow paths: `flow_dependencies` starts with `f`. +/// - Flow-related paths are checked before script paths to avoid false positives, as some +/// flow paths (e.g., `/run_preview_flow`) share prefixes with script paths (e.g., `/run_preview`). /// /// Returns the kind based on the match, or `None` if no match is found. fn determine_kind_from_route(route_path: &str) -> Option { @@ -247,6 +255,12 @@ fn determine_kind_from_route(route_path: &str) -> Option { { return None; } + if DEPENDENCY_JOBS + .iter() + .any(|path| route_path.starts_with(path)) + { + return Some("dependencies".to_string()); + } if AGENT_JOBS.iter().any(|path| route_path.starts_with(path)) { return Some("agents".to_string()); } @@ -586,17 +600,23 @@ pub struct JobReadConfinement { } impl JobReadConfinement { - /// Whether a job that ran `runnable_path` as `kind` (`scripts`, `flows` or `agents`) - /// is inside the confinement. A read path admits every kind; a run scope only its own. + /// Whether a job that ran `runnable_path` as `kind` (`scripts`, `flows`, `agents` or + /// `dependencies`) is inside the confinement. A read path admits every kind; a run + /// scope only its own. pub fn admits(&self, kind: &str, runnable_path: &str) -> bool { if self.read_paths.allows(runnable_path) { return true; } + // Lock jobs and previews store the path their caller sent. The matcher reads `*` + // in it as a pattern, and `,`/`:` are scope syntax: such a path needs a grant + // covering every path, as enqueuing it did. + let resource = windmill_common::auth::is_scope_literal_path(runnable_path) + .then(|| vec![runnable_path.to_string()]); let required = ScopeDefinition::new( ScopeDomain::Jobs.as_str(), ScopeAction::Run.as_str(), Some(kind), - Some(vec![runnable_path.to_string()]), + resource, ); self.run.iter().any(|scope| scope.includes(&required)) } @@ -1715,6 +1735,24 @@ mod tests { scope_for_route("POST", "/api/w/ws/jobs/run/agent/u/x/y").as_deref(), Some("jobs:run:agents") ); + for path in [ + "/api/w/ws/jobs/run/dependencies", + "/api/w/ws/jobs/run/dependencies_async", + "/api/w/ws/jobs/run/flow_dependencies", + "/api/w/ws/jobs/run/flow_dependencies_async", + ] { + assert_eq!( + scope_for_route("POST", path).as_deref(), + Some("jobs:run:dependencies"), + "{path}" + ); + assert!(check_route_access(&["jobs:run:dependencies".into()], path, "POST").is_ok()); + assert!(check_route_access(&["jobs:run:flows".into()], path, "POST").is_err()); + } + // A dependencies grant reaches no other run route. + for path in ["/api/w/ws/jobs/run/preview", "/api/w/ws/jobs/run/p/u/x/y"] { + assert!(check_route_access(&["jobs:run:dependencies".into()], path, "POST").is_err()); + } // Preview/bundle runs have no deployed path and their handlers require the // broad `jobs:run` scope, so the derived scope must not carry a kind. diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index adbbda0abc..f1e4120e8c 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -2010,9 +2010,11 @@ async fn require_job_within_read_scope( } } // `scope_kind` is the runnable kind a `jobs:run::` scope can name, or - // NULL for a job no such scope reaches directly (previews, dependency jobs, - // flow-inlined scripts) — those are still readable as a step of a matching flow, - // through their ancestors. A `singlestepflow` wraps either a script or a flow, so it + // NULL for a job no such scope reaches directly (previews, flow-inlined scripts) — + // those are still readable as a step of a matching flow, through their ancestors. + // A dependency job's path is the one its `jobs:run:dependencies:` check + // admitted, but it is filed under `dependencies`, so it never passes for a run of + // the deployed runnable at that path. A `singlestepflow` wraps either a script or a flow, so it // projects onto the wrapped runnable the same way the batch-rerun query does. An agent // run is a preview of the one-step flow `agent_runs::agent_step_flow` builds, filed under the // agent's path (`.chat` for a chat turn); the editor's own runs of it look the same, @@ -2031,6 +2033,7 @@ async fn require_job_within_read_scope( WHEN a.agent THEN 'agents' WHEN j.kind IN ('script', 'script_hub', 'unassigned_script') THEN 'scripts' WHEN j.kind IN ('flow', 'unassigned_flow') THEN 'flows' + WHEN j.kind IN ('dependencies', 'flowdependencies') THEN 'dependencies' WHEN j.kind IN ('singlestepflow', 'unassigned_singlestepflow') THEN CASE WHEN COALESCE( (SELECT m->'value'->>'type' @@ -4628,14 +4631,8 @@ async fn cancel_selection( } else { ids }; - let Json(mut w_cancelled) = cancel_jobs( - ids, - &db, - &authed, - workspace_id.as_str(), - force_cancel, - ) - .await?; + let Json(mut w_cancelled) = + cancel_jobs(ids, &db, &authed, workspace_id.as_str(), force_cancel).await?; cancelled.append(&mut w_cancelled); } @@ -9529,13 +9526,23 @@ pub struct RunDependenciesResponse { pub dependencies: String, } +/// The path comes from the request body: one carrying scope syntax (`,`, `:`, `*`) could +/// put a granted path beside an ungranted one, so it needs a grant covering every path. +fn check_dependencies_scope(authed: &ApiAuthed, path: &str) -> error::Result<()> { + if windmill_common::auth::is_scope_literal_path(path) { + check_scopes(authed, || format!("jobs:run:dependencies:{path}")) + } else { + check_scopes(authed, || "jobs:run:dependencies".to_string()) + } +} + async fn push_dependencies_job( authed: &ApiAuthed, db: &DB, w_id: &str, req: RunDependenciesRequest, ) -> error::Result { - check_scopes(authed, || format!("jobs:run"))?; + check_dependencies_scope(authed, &req.entrypoint)?; if authed.is_operator { return Err(error::Error::NotAuthorized( "Operators cannot run dependencies jobs for security reasons".to_string(), @@ -9658,7 +9665,7 @@ async fn push_flow_dependencies_job( w_id: &str, req: RunFlowDependenciesRequest, ) -> error::Result { - check_scopes(authed, || format!("jobs:run"))?; + check_dependencies_scope(authed, &req.path)?; check_operator_can_build_flows(db, w_id, authed.is_operator, "run dependencies jobs").await?; // The dependency job locks whatever inline code this request carries, on a worker. A // composition-only flow has none, so validating here costs a builder nothing and keeps the diff --git a/backend/windmill-api/src/token.rs b/backend/windmill-api/src/token.rs index b7fbe96404..ab22f2d15b 100644 --- a/backend/windmill-api/src/token.rs +++ b/backend/windmill-api/src/token.rs @@ -215,6 +215,11 @@ lazy_static! { label: "Run flows".to_string(), requires_resource_path: true, }, + ScopeOption { + value: "jobs:run:dependencies".to_string(), + label: "Generate dependency locks".to_string(), + requires_resource_path: true, + }, ], }]; diff --git a/frontend/src/lib/components/settings/ScopeSelector.svelte b/frontend/src/lib/components/settings/ScopeSelector.svelte index b3293ab36e..663e043035 100644 --- a/frontend/src/lib/components/settings/ScopeSelector.svelte +++ b/frontend/src/lib/components/settings/ScopeSelector.svelte @@ -643,7 +643,7 @@ ? 'bg-surface-disabled opacity-60' : 'bg-surface-tertiary'}" > -
+
-
+ +
Backend
diff --git a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptRunnableByPath.svelte b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptRunnableByPath.svelte index 293d83b4ae..ec6e335ab0 100644 --- a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptRunnableByPath.svelte +++ b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptRunnableByPath.svelte @@ -19,6 +19,7 @@ CtxAppInput } from '../../inputType' import type { AppViewerContext } from '../../types' + import { useOperatorBuilderApps } from '$lib/operatorWriteRights' import { createEventDispatcher } from 'svelte' import { deepEqual } from 'fast-equals' import { computeFields } from './utils' @@ -37,6 +38,7 @@ import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' const operatingWorkspace = useOperatingWorkspace() + const operatorBuilderApps = useOperatorBuilderApps() interface Props { runnable: RunnableByPath @@ -362,16 +364,18 @@ > Edit - + {#if !$operatorBuilderApps} + + {/if} {/if} {/if} - {#if !onlyFlow} - + {#if !$operatorBuilderApps} + {#if !onlyFlow} + + {/if} + {/if} -
diff --git a/frontend/src/lib/components/common/table/AppRow.svelte b/frontend/src/lib/components/common/table/AppRow.svelte index bd881a31b7..50bc64f24b 100644 --- a/frontend/src/lib/components/common/table/AppRow.svelte +++ b/frontend/src/lib/components/common/table/AppRow.svelte @@ -7,6 +7,7 @@ import type ShareModal from '$lib/components/ShareModal.svelte' import { AppService, type ListableApp } from '$lib/gen' import { userStore, userWorkspaces, workspaceStore } from '$lib/stores' + import { useOperatorBuilderApps } from '$lib/operatorWriteRights' import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte' import { createEventDispatcher } from 'svelte' import Button from '../button/Button.svelte' @@ -68,6 +69,7 @@ }: Props = $props() const dispatch = createEventDispatcher() + const operatorBuilderApps = useOperatorBuilderApps() let appExport: { open: (path: string, rawApp?: boolean) => void } | undefined = $state(undefined) let appDeploymentHistory: AppDeploymentHistory | undefined = $state(undefined) @@ -159,24 +161,22 @@ {/snippet} {#snippet actions()} appDeploymentHistory?.open(), - hide: $userStore?.operator + hide: $userStore?.operator && !($operatorBuilderApps && app.raw_app) }, { displayName: 'Permissions', @@ -295,7 +295,7 @@ action: () => { shareModal.openDrawer && shareModal.openDrawer(path, 'app') }, - hide: $userStore?.operator + hide: $userStore?.operator && !($operatorBuilderApps && app.raw_app) }, { displayName: 'Copy path', @@ -341,7 +341,7 @@ }, type: 'delete', disabled: !canEdit, - hide: $userStore?.operator + hide: $userStore?.operator && !($operatorBuilderApps && app.raw_app) } ] }} diff --git a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts index 679f7c8bb2..6c49effb81 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts +++ b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts @@ -168,7 +168,11 @@ import type { ArtifactVersionTarget } from '$lib/components/sessions/previewRout import { appendAttachedFilesRoster } from './files/fileTools' import { ENTER_PLAN_MODE_TOOL, EXIT_PLAN_MODE_TOOL } from './planMode' import { PlanModeController, type PlanModeHost } from './planModeController.svelte' -import { navigationOperatorBuilderFlows } from '$lib/operatorWriteRights' +import { + navigationOperatorBuilderApps, + navigationOperatorBuilderFlows, + navigationOperatorBuilderRights +} from '$lib/operatorWriteRights' // Compaction of the stored history: once the projected request size // (contextTokens — the provider's report when current, a fresh chars/4 @@ -301,7 +305,9 @@ export function supportsPlanMode(mode: AIMode): boolean { return PLAN_MODES.has(mode) } +const isOperatorBuilder = fromStore(navigationOperatorBuilderRights) const isOperatorBuilderFlows = fromStore(navigationOperatorBuilderFlows) +const isOperatorBuilderApps = fromStore(navigationOperatorBuilderApps) export function isAIModeVisible(mode: AIMode): boolean { return mode !== AIMode.GLOBAL || isGlobalAiEnabled() @@ -1453,20 +1459,23 @@ export class AIChatManager implements ChatViewHost { .map((s) => ({ ...s, kind: 'skill' as const })) ]) - // The flow and script builders both write code, which the backend refuses from an operator - // with the builder right: leaving them reachable would only produce work that cannot be + // The flow, app and script builders all write code, which the backend refuses from an + // operator with builder rights: leaving them reachable would only produce work that cannot be // deployed. allowedModes: Record = $derived({ script: this.flowAiChatHelpers === undefined && this.scriptEditorOptions !== undefined && !this.disabledModes.script && - !isOperatorBuilderFlows.current, + !isOperatorBuilder.current, flow: this.flowAiChatHelpers !== undefined && !this.disabledModes.flow && !isOperatorBuilderFlows.current, - app: this.appAiChatHelpers !== undefined && !this.disabledModes.app, + app: + this.appAiChatHelpers !== undefined && + !this.disabledModes.app && + !isOperatorBuilderApps.current, navigator: !this.disabledModes.navigator, ask: !this.disabledModes.ask, API: !this.disabledModes.API, diff --git a/frontend/src/lib/components/copilot/chat/global/core.ts b/frontend/src/lib/components/copilot/chat/global/core.ts index 8cfa0ffffe..b6eac0439c 100644 --- a/frontend/src/lib/components/copilot/chat/global/core.ts +++ b/frontend/src/lib/components/copilot/chat/global/core.ts @@ -75,6 +75,8 @@ import { formatDataTableRef } from '$lib/components/raw_apps/dataTableRefUtils' import { appSourceToDraftValue } from '$lib/components/raw_apps/rawAppDraftValue' +import { conformBuilderAppPolicy } from '$lib/components/raw_apps/builderAppPolicy' +import { operatorBuilderAppsIn } from '$lib/operatorWriteRights' import type { RawAppDomQuery } from '$lib/components/raw_apps/rawAppDom' import { dataUrlToImagePart, normalizeImageDataUrl, type AttachedImage } from '../imageUtils' import { sanitizeAttachmentName, textLineCount, type AttachedTextFile } from '../textFileUtils' @@ -2069,6 +2071,11 @@ async function saveAppDraft( path: string, value: AppDraftValue ): Promise { + if (operatorBuilderAppsIn(workspace)) { + const role = await roleForWorkspace(workspace) + value.policy ??= {} + conformBuilderAppPolicy(value.policy, role.kind === 'resolved' ? role.user : undefined) + } return saveGlobalAppDraft(workspace, path, value) } diff --git a/frontend/src/lib/components/details/FlowDetail.svelte b/frontend/src/lib/components/details/FlowDetail.svelte index d3406721de..c755136748 100644 --- a/frontend/src/lib/components/details/FlowDetail.svelte +++ b/frontend/src/lib/components/details/FlowDetail.svelte @@ -432,7 +432,7 @@ return buttons } - // The builder right covers flows only; building an app is still refused to operators. + // Build app makes a low-code app, which no builder right covers. if (!actingUser?.operator) { buttons.push({ label: 'Build app', diff --git a/frontend/src/lib/components/home/CreateActionsMenu.svelte b/frontend/src/lib/components/home/CreateActionsMenu.svelte index 7a7615546a..d858e91e31 100644 --- a/frontend/src/lib/components/home/CreateActionsMenu.svelte +++ b/frontend/src/lib/components/home/CreateActionsMenu.svelte @@ -314,10 +314,12 @@ } } - // Only the kinds this user's role authors: a builder operator gets flows alone. + // Only the kinds this user's role authors: a builder operator gets the kinds its rights grant. // Derived, not computed once: switching workspace only sets `workspaceStore`, it does not // remount this component, so a snapshot would keep the previous workspace's kinds. - const options: Option[] = $derived(allOptions.filter((o) => editRights.roleCanAuthor(o.key))) + const options: Option[] = $derived( + allOptions.filter((o) => editRights.roleCanAuthor(o.key === 'app-fullcode' ? 'raw_app' : o.key)) + ) // the doc panel only shows while an option is hovered or focused, so the menu opens compact let activeKey: string | undefined = $state(undefined) diff --git a/frontend/src/lib/components/home/ItemsList.svelte b/frontend/src/lib/components/home/ItemsList.svelte index 8e6ab769be..ee22e1a43a 100644 --- a/frontend/src/lib/components/home/ItemsList.svelte +++ b/frontend/src/lib/components/home/ItemsList.svelte @@ -19,7 +19,7 @@ import { resource } from 'runed' import { getDraftItems } from '$lib/workspaceDrafts.svelte' import { disableHubStore, userStore, workspaceStore } from '$lib/stores' - import { useEditRights, useOperatorBuilderFlows } from '$lib/operatorWriteRights' + import { useEditRights } from '$lib/operatorWriteRights' import type uFuzzy from '@leeoniya/ufuzzy' import { ArrowDownUp, @@ -64,7 +64,6 @@ import BulkActionsBar from './BulkActionsBar.svelte' import { HomeSelection, setHomeSelection, toBulkItem } from './homeSelection.svelte' - const operatorBuilderFlows = useOperatorBuilderFlows() const editRights = useEditRights() interface Props { @@ -358,7 +357,11 @@ const base = { ...it, canWrite: - editRights.canEditItem(it.type, it.path, (it.extra_perms ?? {}) as any) && + editRights.canEditItem( + it.type === 'app' && it.raw_app ? 'raw_app' : it.type, + it.path, + (it.extra_perms ?? {}) as any + ) && (it.type === 'script' || it.workspace_id == $workspaceStore) } // combinedItems reads a script's time from `created_at`; the endpoint's @@ -1209,8 +1212,10 @@ * whose direct-deploy protection cleared `showEditButtons` — must not be shown them. * Reading archived items is not a write, so it is not gated on this. */ - // A flow is the one kind every author can create, the builder right included. - let canCreateHere = $derived(editRights.roleCanAuthor('flow') && showEditButtons) + // Every author can create a flow or a full-code app, except a builder granted only the other. + let canCreateHere = $derived( + (editRights.roleCanAuthor('flow') || editRights.roleCanAuthor('raw_app')) && showEditButtons + ) // The workspace itself holds nothing — no filter is narrowing the list away. It stays // false until the first load resolves: a skeleton already means "loading", and the @@ -2041,7 +2046,7 @@ script and flow hub pickers observe. Nor for a builder: a hub project brings scripts and apps along. --> (hubPickerOpen = true)} /> diff --git a/frontend/src/lib/components/home/WorkspaceEmptyState.svelte b/frontend/src/lib/components/home/WorkspaceEmptyState.svelte index 1f7bebb1ce..6e828eee34 100644 --- a/frontend/src/lib/components/home/WorkspaceEmptyState.svelte +++ b/frontend/src/lib/components/home/WorkspaceEmptyState.svelte @@ -4,11 +4,11 @@ import Popover from '$lib/components/meltComponents/Popover.svelte' import type { HubProjectPick } from '$lib/hubProject' import { disableHubStore } from '$lib/stores' - import { useOperatorBuilderFlows } from '$lib/operatorWriteRights' + import { useOperatorBuilderRights } from '$lib/operatorWriteRights' import CreateActionsMenu from './CreateActionsMenu.svelte' import HubTemplatePicker from './HubTemplatePicker.svelte' - const operatorBuilderFlows = useOperatorBuilderFlows() + const operatorBuilderRights = useOperatorBuilderRights() interface Props { /** A project was chosen here. The list owns the import dialog, and opens it on this. */ @@ -39,7 +39,7 @@ const rowOpacities = [1, 0.7, 0.4] // A builder gets no hub template: a hub project brings scripts and apps along. - const showHub = $derived(!$disableHubStore && !$operatorBuilderFlows) + const showHub = $derived(!$disableHubStore && !$operatorBuilderRights) // The inline "create a new one" link is the anchor for the very same New menu the // toolbar button opens, so the menu pops next to the words that promised it. diff --git a/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte b/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte index 0928ddce7a..13c0eb4303 100644 --- a/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte @@ -25,7 +25,8 @@ import { Alert, Badge } from '$lib/components/common' import { copilotInfo, copilotWorkspace } from '$lib/aiStore' import { loadCopilot } from '$lib/components/copilot/loadCopilot' - import { react18Template, react19Template, svelte5Template } from './templates' + import { BUILDER_FRAMEWORK_TEMPLATES, FRAMEWORK_TEMPLATES } from './templates' + import { conformBuilderAppPolicy } from './builderAppPolicy' import type { Runnable } from './rawAppPolicy' import { type DataTableRef, @@ -46,6 +47,8 @@ import RawAppDataTableDrawer from './RawAppDataTableDrawer.svelte' import FileEditorIcon from './FileEditorIcon.svelte' import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' + import { useOperatorBuilderApps } from '$lib/operatorWriteRights' + import { prefersSessionHandoff } from '$lib/components/copilot/chat/global/gate' export type RawAppTemplatePickerResult = { files: Record @@ -65,11 +68,14 @@ onStart: (result: RawAppTemplatePickerResult, mode: RawAppBuildMode) => void } = $props() - const templates = [ - { name: 'React 19', icon: 'tsx', files: react19Template, recommended: true }, - { name: 'React 18', icon: 'tsx', files: react18Template }, - { name: 'Svelte 5', icon: 'svelte', files: svelte5Template } - ] + const templates = $derived.by(() => { + const files = $operatorBuilderApps ? BUILDER_FRAMEWORK_TEMPLATES : FRAMEWORK_TEMPLATES + return [ + { name: 'React 19', icon: 'tsx', files: files.react19, recommended: true }, + { name: 'React 18', icon: 'tsx', files: files.react18 }, + { name: 'Svelte 5', icon: 'svelte', files: files.svelte5 } + ] + }) let selectedTemplateIndex = $state(0) let selectedDatatable = $state(undefined) @@ -82,6 +88,7 @@ let dataTableDrawer: RawAppDataTableDrawer | undefined = $state() const operatingWorkspace = useOperatingWorkspace() + const operatorBuilderApps = useOperatorBuilderApps() let opWs = $derived($operatingWorkspace) const datatables = createDatatablesResource(() => opWs) @@ -275,21 +282,27 @@ } }) - // With AI turned off for the workspace there is nothing to choose between. - const aiOffered = $derived(!$copilotInfo.workspaceDisabled) + // With AI turned off for the workspace there is nothing to choose between. A builder is + // offered AI only through a session: the docked app mode writes inline code they cannot save. + const aiOffered = $derived( + !$copilotInfo.workspaceDisabled && + (!$operatorBuilderApps || prefersSessionHandoff($userStore?.operator)) + ) const canStart = $derived( !!templates[selectedTemplateIndex] && !newSchemaAlreadyExists && rolesSettled && accessSettled ) function appPolicy(): Policy { - return { + const policy: Policy = { on_behalf_of: $userStore?.username.includes('@') ? $userStore?.username : `u/${$userStore?.username}`, on_behalf_of_email: $userStore?.email, execution_mode: 'publisher' } + if ($operatorBuilderApps) conformBuilderAppPolicy(policy, $userStore) + return policy } const startCardClasses = diff --git a/frontend/src/lib/components/raw_apps/builderAppPolicy.test.ts b/frontend/src/lib/components/raw_apps/builderAppPolicy.test.ts new file mode 100644 index 0000000000..b24f319240 --- /dev/null +++ b/frontend/src/lib/components/raw_apps/builderAppPolicy.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest' +import { conformBuilderAppPolicy } from './builderAppPolicy' + +const builder = { username: 'op', email: 'op@x.dev' } + +describe('conformBuilderAppPolicy', () => { + it('conforms a policy loaded from an app someone else deployed', () => { + const policy: Record = { + sandbox: false, + execution_mode: 'viewer', + on_behalf_of: 'u/admin', + on_behalf_of_email: 'admin@x.dev' + } + conformBuilderAppPolicy(policy, builder) + expect(policy).toEqual({ sandbox: true, execution_mode: 'publisher' }) + }) + + it("keeps the builder's own identity", () => { + const policy: Record = { + sandbox: true, + on_behalf_of: 'u/op', + on_behalf_of_email: 'op@x.dev' + } + conformBuilderAppPolicy(policy, builder) + expect(policy).toEqual({ sandbox: true, on_behalf_of: 'u/op', on_behalf_of_email: 'op@x.dev' }) + }) +}) diff --git a/frontend/src/lib/components/raw_apps/builderAppPolicy.ts b/frontend/src/lib/components/raw_apps/builderAppPolicy.ts new file mode 100644 index 0000000000..967b56f971 --- /dev/null +++ b/frontend/src/lib/components/raw_apps/builderAppPolicy.ts @@ -0,0 +1,23 @@ +/** + * Brings a raw-app policy in line with what the backend requires of a draft from an operator with + * builder rights (see `docs/operator-builder-rights.md`): sandboxed, never in Viewer mode, and + * running as nobody but its builder. A policy loaded from an app someone else deployed carries + * their identity and may carry either of the other two, so every builder draft writer goes + * through this before saving. Writes only what differs, so it is safe inside an `$effect`. + */ +export function conformBuilderAppPolicy( + policy: Record, + user: { username?: string; email?: string } | undefined +): void { + if (policy.sandbox !== true) policy.sandbox = true + if (policy.execution_mode === 'viewer') policy.execution_mode = 'publisher' + const username = user?.username + const self = username?.includes('@') ? username : `u/${username}` + if ( + (policy.on_behalf_of != undefined && policy.on_behalf_of !== self) || + (policy.on_behalf_of_email != undefined && policy.on_behalf_of_email !== user?.email) + ) { + delete policy.on_behalf_of + delete policy.on_behalf_of_email + } +} diff --git a/frontend/src/lib/components/raw_apps/templates.ts b/frontend/src/lib/components/raw_apps/templates.ts index d6df109262..50eae99e97 100644 --- a/frontend/src/lib/components/raw_apps/templates.ts +++ b/frontend/src/lib/components/raw_apps/templates.ts @@ -1179,6 +1179,271 @@ export const FRAMEWORK_TEMPLATES = { export type FrameworkKey = keyof typeof FRAMEWORK_TEMPLATES +// An operator with builder rights gets no STARTER_RUNNABLES (they are inline code), so their +// starter calls nothing and explains how to wire runnables already deployed in the workspace. +const BUILDER_USES = [ + { + title: 'Forms for your scripts', + text: 'Give a script a simple form, so anyone can run it without learning Windmill.' + }, + { + title: 'Dashboards', + text: 'Show what a script or flow returns as a table or a chart, fresh every time the app opens.' + }, + { + title: 'Self-service tools', + text: 'Share the app with your team or by link. It runs its scripts as you, so viewers need no access to them.' + } +] + +const BUILDER_SAFETY = + 'Your app can only run scripts and flows already deployed in this workspace, and it runs isolated from each viewer’s session.' + +const BUILDER_STEPS = [ + { + title: 'Pick what it runs', + description: 'Choose a script or flow already deployed in this workspace.', + path: ['Backend panel', '+', 'Select a script or flow'], + note: 'You only see the ones you can read. Each one gets an id, such as a.' + }, + { + title: 'Show it on the page', + description: 'Each runnable becomes a function the page can call by its id.', + note: 'Working with an AI assistant? Tell it the id and what to show. This is the line it writes.', + code: "import { backend } from './wmill'\n\nconst res = await backend.a({ name: 'world' })" + }, + { + title: 'Run long jobs in the background', + description: 'backendAsync starts a job without making the page wait.', + note: 'Check on it with getJob, or follow its output as it runs with streamJob.', + code: "import { backendAsync, getJob } from './wmill'\n\nconst id = await backendAsync.a({ name: 'world' })\nconst job = await getJob(id)" + } +] + +const BUILDER_CONTEXT_CODE = 'const ctx = (window as any).ctx\n\nctx.workspace, ctx.ctx?.username' + +const builderAppTsx = `import React from 'react' +import './index.css' + +// Windmill injects the viewer (absent for anonymous visitors) and the workspace. +const ctx = (window as any).ctx ?? {} +const viewer: { name?: string; username?: string } | undefined = ctx.ctx +const firstName = (viewer?.name || viewer?.username || '').split(' ')[0] + +type Step = { title: string; description: string; note: string; path?: string[]; code?: string } + +const uses: { title: string; text: string }[] = ${JSON.stringify(BUILDER_USES, null, 2)} + +const steps: Step[] = ${JSON.stringify(BUILDER_STEPS, null, 2)} + +const sectionTitle: React.CSSProperties = { + margin: '32px 0 12px', + fontSize: 12, + fontWeight: 600, + letterSpacing: '0.06em', + textTransform: 'uppercase', + color: 'var(--muted)' +} + +function Card(props: { step: number; title: string; description: string; code?: string; children: React.ReactNode }) { + return ( +
+
+ {props.step} +
+

{props.title}

+

{props.description}

+
+
+
{props.children}
+ {props.code && ( +
+          {props.code}
+        
+ )} +
+ ) +} + +const App = () => { + return ( +
+
+ Windmill app{ctx.workspace ? ' · ' + ctx.workspace : ''} +

{firstName ? 'Welcome, ' + firstName : 'Welcome'}

+

+ Turn the scripts and flows deployed in this workspace into a tool anyone can use. Edit + App.tsx to make it yours. +

+
+

What you can build

+
+ {uses.map((use) => ( +
+

{use.title}

+

{use.text}

+
+ ))} +
+

+ {${JSON.stringify(BUILDER_SAFETY)}} +

+

How it works

+
+ {steps.map((step, i) => ( + + {step.path && ( +
+ {step.path.map((label, j) => ( + + {j > 0 && →} + {label} + + ))} +
+ )} + {step.note} +
+ ))} + + {ctx.workspace ? ( + <> +
+ Workspace + {ctx.workspace} +
+
+ Viewer + {viewer ? viewer.name || viewer.username : 'Anonymous visitor'} +
+ + ) : ( + The editor preview has no viewer. Deploy the app to see yours here. + )} +
+
+
+ ) +} + +export default App +` + +const builderAppSvelte = ` + +{#snippet header(step: number, title: string, description: string)} +
+ {step} +
+

{title}

+

{description}

+
+
+{/snippet} + +
+
+ Windmill app{ctx.workspace ? ' · ' + ctx.workspace : ''} +

{firstName ? 'Welcome, ' + firstName : 'Welcome'}

+

+ Turn the scripts and flows deployed in this workspace into a tool anyone can use. Edit + App.svelte to make it yours. +

+
+ +

What you can build

+
+ {#each uses as use (use.title)} +
+

{use.title}

+

{use.text}

+
+ {/each} +
+

{${JSON.stringify(BUILDER_SAFETY)}}

+ +

How it works

+
+ {#each steps as step, i (step.title)} +
+ {@render header(i + 1, step.title, step.description)} +
+ {#if step.path} +
+ {#each step.path as label, j (label)} + {#if j > 0}→{/if} + {label} + {/each} +
+ {/if} + {step.note} +
+ {#if step.code} +
{step.code}
+ {/if} +
+ {/each} + +
+ {@render header(steps.length + 1, 'Know who is viewing', 'Windmill passes the workspace and the viewer to the app.')} +
+ {#if ctx.workspace} +
Workspace {ctx.workspace}
+
+ Viewer + {viewer ? viewer.name || viewer.username : 'Anonymous visitor'} +
+ {:else} + The editor preview has no viewer. Deploy the app to see yours here. + {/if} +
+
{contextCode}
+
+
+
+ + +` + +export const BUILDER_FRAMEWORK_TEMPLATES = { + react19: { ...react19Template, '/App.tsx': builderAppTsx }, + react18: { ...react18Template, '/App.tsx': builderAppTsx }, + svelte5: { ...svelte5Template, '/App.svelte': builderAppSvelte } +} + type StarterArg = { type: 'string' | 'number' } function inlineRunnable( diff --git a/frontend/src/lib/components/sessions/RawAppEditorView.svelte b/frontend/src/lib/components/sessions/RawAppEditorView.svelte index de05359bd5..05b8ea6a2f 100644 --- a/frontend/src/lib/components/sessions/RawAppEditorView.svelte +++ b/frontend/src/lib/components/sessions/RawAppEditorView.svelte @@ -16,7 +16,11 @@ import DiffDrawer from '$lib/components/DiffDrawer.svelte' import type { WorkspaceItem } from '$lib/components/workspacePicker' import { removeSession, type SessionRuntime } from './sessionRuntime.svelte' - import { FRAMEWORK_TEMPLATES, STARTER_RUNNABLES } from '$lib/components/raw_apps/templates' + import { + BUILDER_FRAMEWORK_TEMPLATES, + FRAMEWORK_TEMPLATES, + STARTER_RUNNABLES + } from '$lib/components/raw_apps/templates' import SessionEditorTarget from './SessionEditorTarget.svelte' import { runResetToDeployed } from '$lib/userDraftToast' import { invalidateWorkspaceDrafts } from '$lib/workspaceDrafts.svelte' @@ -63,7 +67,10 @@ const files = cell.store.val?.files if (!newRawApp || (cell.saved.val && !cell.saved.val.no_deployed) || !files) return false const count = Object.keys(files).length - return Object.values(FRAMEWORK_TEMPLATES).some((template: Record) => { + return [ + ...Object.values(FRAMEWORK_TEMPLATES), + ...Object.values(BUILDER_FRAMEWORK_TEMPLATES) + ].some((template: Record) => { const keys = Object.keys(template) return count === keys.length && keys.every((k) => files[k] === template[k]) }) diff --git a/frontend/src/lib/components/settings/WorkspaceOperatorSettings.svelte b/frontend/src/lib/components/settings/WorkspaceOperatorSettings.svelte index da7cd5a3de..b9ad80f63e 100644 --- a/frontend/src/lib/components/settings/WorkspaceOperatorSettings.svelte +++ b/frontend/src/lib/components/settings/WorkspaceOperatorSettings.svelte @@ -31,6 +31,7 @@ // Kept out of `operatorWorkspaceSettings` so the visibility table's "Enable all" never flips a // write right, and so these rows stay out of that table. let builderFlows = $state(false) + let builderApps = $state(false) // Withdrawable rather than granted: operators hold these until an admin turns them off. let manageSchedules = $state(true) let manageTriggers = $state(true) @@ -38,6 +39,7 @@ let originalSettings = $state({ ...untrack(() => operatorWorkspaceSettings), builder_flows: false, + builder_apps: false, manage_schedules: true, manage_triggers: true }) @@ -51,15 +53,24 @@ const settingsPayload = $derived({ ...operatorWorkspaceSettings, builder_flows: builderFlows, + builder_apps: builderApps, manage_schedules: manageSchedules, manage_triggers: manageTriggers }) - // The seat cost lands when the right is first granted, so confirm only on that transition. - const grantsBuilderRight = $derived(builderFlows && !originalSettings.builder_flows) + // The seat cost lands on the first right granted, so confirm only when going from neither to + // either. Adding the second one later is already paid for. + const grantsFirstBuilderRight = $derived( + (builderFlows || builderApps) && + !(originalSettings.builder_flows || originalSettings.builder_apps) + ) + + const grantedBuilderKinds = $derived( + [builderFlows && 'flows', builderApps && 'full-code apps'].filter(Boolean).join(' and ') + ) function onSaveClicked() { - if (grantsBuilderRight) { + if (grantsFirstBuilderRight) { confirmBuilderOpen = true } else { saveSettings() @@ -105,17 +116,20 @@ // would be saved here with the next unrelated change, builder rights among them. const { builder_flows: remoteFlows, + builder_apps: remoteApps, manage_schedules: remoteSchedules, manage_triggers: remoteTriggers, ...remoteVisibility } = settings.operator_settings ?? {} operatorWorkspaceSettings = { ...defaultVisibility, ...remoteVisibility } builderFlows = remoteFlows ?? false + builderApps = remoteApps ?? false manageSchedules = remoteSchedules ?? true manageTriggers = remoteTriggers ?? true originalSettings = { ...operatorWorkspaceSettings, builder_flows: builderFlows, + builder_apps: builderApps, manage_schedules: manageSchedules, manage_triggers: manageTriggers } @@ -155,8 +169,8 @@
@@ -165,6 +179,11 @@ options={{ right: 'Operators can build flows' }} size="xs" /> +
- They can create, edit and delete flows wherever their folder permissions already let them - write. Review those permissions before enabling. + They can create, edit and delete {grantedBuilderKinds} wherever their folder permissions already + let them write. Review those permissions before enabling.
diff --git a/frontend/src/lib/editRights.test.ts b/frontend/src/lib/editRights.test.ts index a8e9b7eafa..faa853e708 100644 --- a/frontend/src/lib/editRights.test.ts +++ b/frontend/src/lib/editRights.test.ts @@ -26,6 +26,8 @@ describe('edit rights', () => { expect(roleCanAuthor('flow', user({}), builder)).toBe(true) expect(roleCanAuthor('script', user({}), builder)).toBe(false) expect(roleCanAuthor('flow', user({}), undefined)).toBe(false) + expect(roleCanDraft('raw_app', user({}), builder)).toBe(false) + expect(roleCanDraft('raw_app', user({}), { builder_apps: true })).toBe(true) }) // drafts.rs admits an admin before its operator branch; the item handlers do not. diff --git a/frontend/src/lib/editRights.ts b/frontend/src/lib/editRights.ts index 3034840a6e..9073a03d28 100644 --- a/frontend/src/lib/editRights.ts +++ b/frontend/src/lib/editRights.ts @@ -26,7 +26,10 @@ export type EditKind = | (string & {}) type OperatorRights = - | Pick, 'builder_flows' | 'manage_schedules' | 'manage_triggers'> + | Pick< + NonNullable, + 'builder_flows' | 'builder_apps' | 'manage_schedules' | 'manage_triggers' + > | null | undefined @@ -35,8 +38,9 @@ type OperatorRights = * in a workspace with these `operator_settings`: the role half of every check against the item * handlers. Saving a draft is `roleCanDraft`'s rule, and which items they may write is * `canEditItem`'s other half. For an operator: - * - code (scripts, apps, agents, pipelines): never; - * - flows: where the workspace grants `builder_flows` (`check_operator_can_build_flows`); + * - code (scripts, low-code apps, agents, pipelines): never; + * - flows and full-code apps: where the workspace grants `builder_flows` / `builder_apps` + * (`check_operator_can_build`); * - schedules and triggers: unless the workspace set `manage_*` to false — a withdrawable * right, so an unset key grants it (docs/operator-write-rights.md); * - resources, variables and groups: always, their permissions alone decide. @@ -49,7 +53,8 @@ export function roleCanAuthor( if (!user?.operator) return true switch (kind) { case 'flow': - return operatorSettings?.builder_flows === true + case 'raw_app': + return builderGrants(kind, operatorSettings) case 'schedule': return operatorSettings?.manage_schedules !== false case 'trigger': @@ -66,8 +71,8 @@ export function roleCanAuthor( /** * Whether the user may save drafts of `kind` — what every session editor writes. Mirrors drafts.rs * `require_can_write_path`, which differs from the item handlers both ways: it admits an admin - * before its operator branch, and it refuses every operator draft but a flow under - * `builder_flows` — schedules, triggers, resources and variables included, though an operator may + * before its operator branch, and it refuses every operator draft but a flow or full-code app + * under its builder right — schedules, triggers, resources and variables included, though an operator may * write those directly. */ export function roleCanDraft( @@ -76,7 +81,14 @@ export function roleCanDraft( operatorSettings: OperatorRights ): boolean { if (user?.is_admin || user?.is_super_admin || !user?.operator) return true - return kind === 'flow' && operatorSettings?.builder_flows === true + return builderGrants(kind, operatorSettings) +} + +function builderGrants(kind: EditKind, operatorSettings: OperatorRights): boolean { + return ( + (kind === 'flow' && operatorSettings?.builder_flows === true) || + (kind === 'raw_app' && operatorSettings?.builder_apps === true) + ) } /** `roleCanDraft` for the item at `path`, with its own permissions as in `canEditItem`. */ diff --git a/frontend/src/lib/operatorWriteRights.ts b/frontend/src/lib/operatorWriteRights.ts index 6b6aac510a..916e39dce6 100644 --- a/frontend/src/lib/operatorWriteRights.ts +++ b/frontend/src/lib/operatorWriteRights.ts @@ -1,5 +1,5 @@ -import { derived, fromStore, type Readable } from 'svelte/store' -import { userWorkspaces, workspaceStore } from '$lib/stores' +import { derived, fromStore, get, readable, type Readable } from 'svelte/store' +import { userWorkspaces, usersWorkspaceStore, workspaceStore } from '$lib/stores' import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' import { useActingUser } from '$lib/actingUser.svelte' import { canDraftItem, canEditItem, roleCanAuthor, roleCanDraft } from '$lib/editRights' @@ -54,22 +54,57 @@ export const useTriggerLock = () => writeLock('trigger', 'triggers') /** * True when the user is an operator of `workspace` and it granted operators the right to compose - * flows out of runnables that are already deployed. They still author no code, and everywhere else - * `operator` keeps meaning read-only, so a gate on the operator role has to consult this before + * this kind out of runnables that are already deployed. They still author no code, and everywhere + * else `operator` keeps meaning read-only, so a gate on the operator role has to consult this before * refusing. `operator_settings` is null for a non-operator, so this is false for them. + * + * The two rights are granted independently: anything that authors one kind must gate on that + * kind, or a flows-only workspace offers app affordances the backend then refuses. */ -function builderFlows(workspace: Readable): Readable { +function builderRight( + key: 'builder_flows' | 'builder_apps', + workspace: Readable +): Readable { return derived( [userWorkspaces, workspace], ([$userWorkspaces, $workspace]) => - $userWorkspaces.find((w) => w.id === $workspace)?.operator_settings?.builder_flows === true + $userWorkspaces.find((w) => w.id === $workspace)?.operator_settings?.[key] === true + ) +} + +/** Either right, only for surfaces that are not per-kind, such as the create menu's visibility. */ +function anyBuilderRight(workspace: Readable): Readable { + return derived( + [builderRight('builder_flows', workspace), builderRight('builder_apps', workspace)], + ([flows, apps]) => flows || apps ) } /** Builder rights in the operating workspace. Reads context: call during component * initialisation. */ -export const useOperatorBuilderFlows = () => builderFlows(useOperatingWorkspace()) +export const useOperatorBuilderFlows = () => builderRight('builder_flows', useOperatingWorkspace()) +/** Reads context: call during component initialisation. */ +export const useOperatorBuilderApps = () => builderRight('builder_apps', useOperatingWorkspace()) +/** Reads context: call during component initialisation. */ +export const useOperatorBuilderRights = () => anyBuilderRight(useOperatingWorkspace()) /** Builder rights in the navigation workspace, for code outside any component: the legacy AI * chat is one instance for the whole app and acts on the workspace the nav is on. */ -export const navigationOperatorBuilderFlows = builderFlows(workspaceStore) +export const navigationOperatorBuilderFlows = builderRight('builder_flows', workspaceStore) +export const navigationOperatorBuilderApps = builderRight('builder_apps', workspaceStore) +export const navigationOperatorBuilderRights = anyBuilderRight(workspaceStore) + +/** The app builder right in a given workspace, read once: for code acting on an explicit + * workspace rather than the nav's, such as the global AI chat. */ +export const operatorBuilderAppsIn = (workspace: string | undefined) => + get(builderRight('builder_apps', readable(workspace))) + +/** Resolves once the workspace list has loaded. Every builder right reads false until then, so + * code that picks what a builder starts with on a fresh page load has to wait for it. */ +export async function workspaceListLoaded(): Promise { + let unsubscribe = () => {} + await new Promise((resolve) => { + unsubscribe = usersWorkspaceStore.subscribe((list) => list && resolve()) + }) + unsubscribe() +} diff --git a/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte index 9ed2ca296a..ee2bb8fdf7 100644 --- a/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte @@ -4,6 +4,8 @@ import { AppService } from '$lib/gen' import { userStore, workspaceStore } from '$lib/stores' + import { useOperatorBuilderApps, workspaceListLoaded } from '$lib/operatorWriteRights' + import { conformBuilderAppPolicy } from '$lib/components/raw_apps/builderAppPolicy' import { readFieldsRecursively } from '$lib/utils' import { goto } from '$lib/navigation' import { sendUserToast } from '$lib/toast' @@ -32,7 +34,11 @@ type RawAppBuildMode, type RawAppTemplatePickerResult } from '$lib/components/raw_apps/RawAppTemplatePicker.svelte' - import { react19Template, STARTER_RUNNABLES } from '$lib/components/raw_apps/templates' + import { + BUILDER_FRAMEWORK_TEMPLATES, + react19Template, + STARTER_RUNNABLES + } from '$lib/components/raw_apps/templates' import { aiChatManager, AIMode } from '$lib/components/copilot/chat/AIChatManager.svelte' type RawAppDraft = { @@ -97,6 +103,8 @@ // edits to the just-deployed app autosaved to a dead key (autosave appeared // broken). See /scripts/edit, which derives its draft path the same way. // effectivePath omitted: the live-editor-draft entry is owned by RawAppEditor. + const operatorBuilderApps = useOperatorBuilderApps() + const draftSync = usePageDraftSync({ itemKind: 'raw_app', path: () => page.params.path ?? '', @@ -188,6 +196,8 @@ // `?new_draft` loads the saved draft instead of blanking it — see // shouldSeedNewDraft. if (shouldSeedNewDraft(page.url.searchParams, $workspaceStore, 'raw_app', path)) { + await workspaceListLoaded() + if (tok !== loadAppToken) return isNewApp = true // Page reused across same-route nav: clear the previous path's // draft-presence state so it doesn't bleed onto the fresh draft. @@ -270,8 +280,10 @@ } // Seed the React 19 template so the editor has a usable state even if the // user dismisses the picker without selecting. - const seedFiles = { ...react19Template } - const seedRunnables = structuredClone(STARTER_RUNNABLES) + const seedFiles = { + ...($operatorBuilderApps ? BUILDER_FRAMEWORK_TEMPLATES.react19 : react19Template) + } + const seedRunnables = starterRunnables() savedApp = { summary: '', value: { files: seedFiles as any, runnables: seedRunnables as any }, @@ -520,10 +532,22 @@ let rawAppEditor: RawAppEditor | undefined = $state() + // The starter runnables are inline scripts, which the backend refuses from an operator with + // builder rights: seeding them would make their very first deploy fail. + function starterRunnables() { + return $operatorBuilderApps ? {} : structuredClone(STARTER_RUNNABLES) + } + + // The deploy panel cannot switch Viewer mode off, nor drop the identity an app another user + // deployed carries, so the editor conforms the policy as soon as it opens. + $effect(() => { + if ($operatorBuilderApps && policy) conformBuilderAppPolicy(policy, $userStore) + }) + async function onTemplatePickerStart(result: RawAppTemplatePickerResult, mode: RawAppBuildMode) { const picked = { files: { ...result.files }, - runnables: { ...result.runnables, ...structuredClone(STARTER_RUNNABLES) }, + runnables: { ...result.runnables, ...starterRunnables() }, data: result.data, summary: '', policy: result.policy From d3ddef0b7ec4705aa578c4eca24b96e9152485bd Mon Sep 17 00:00:00 2001 From: hugocasa Date: Fri, 9 Oct 2026 20:00:50 +0200 Subject: [PATCH 05/20] feat: retry transient AI provider failures before output reaches readers (#11623) * feat: retry transient AI provider failures before output reaches readers Co-Authored-By: Claude Opus 5.5 * fix: keep completed AI streams, stop retrying prompt blocks, bound retry-after Co-Authored-By: Claude Opus 5.5 * refactor: share the AI send-error retry rule between agent and proxy Co-Authored-By: Claude Opus 5.5 * fix: accept a clean end of stream from OpenAI-compatible providers Co-Authored-By: Claude Opus 5.5 * fix: classify AI provider errors the way the reference clients do Co-Authored-By: Claude Opus 5.5 * fix: count a Gemini function call as the end of its turn Co-Authored-By: Claude Opus 5.5 * fix: count an Anthropic stop_reason as the end of its stream Co-Authored-By: Claude Opus 5.5 * fix: accept a Gemini answer that reports usage and no output Co-Authored-By: Claude Opus 5.5 * fix: accept a Gemini stream that closes cleanly with no output Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Ruben Fiszel --- backend/Cargo.lock | 1 + backend/windmill-ai/Cargo.toml | 1 + backend/windmill-ai/src/ai_bedrock.rs | 23 + backend/windmill-ai/src/ai_google.rs | 56 +++ backend/windmill-ai/src/lib.rs | 1 + backend/windmill-ai/src/providers/bedrock.rs | 55 +- .../windmill-ai/src/providers/google_ai.rs | 53 +- backend/windmill-ai/src/providers/other.rs | 1 + backend/windmill-ai/src/retry.rs | 474 ++++++++++++++++++ backend/windmill-ai/src/sse.rs | 244 ++++++++- backend/windmill-api/src/ai.rs | 17 +- .../src/ai/stream_event_processor.rs | 20 +- backend/windmill-worker/src/ai_executor.rs | 58 ++- 13 files changed, 926 insertions(+), 78 deletions(-) create mode 100644 backend/windmill-ai/src/retry.rs diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 758627445d..64a152c962 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -14935,6 +14935,7 @@ dependencies = [ "indexmap 2.14.2", "lazy_static", "mime_guess", + "rand 0.9.5", "reqwest 0.13.5", "serde", "serde_json", diff --git a/backend/windmill-ai/Cargo.toml b/backend/windmill-ai/Cargo.toml index b1dea3ae54..44c4ac83c7 100644 --- a/backend/windmill-ai/Cargo.toml +++ b/backend/windmill-ai/Cargo.toml @@ -28,6 +28,7 @@ eventsource-stream.workspace = true futures.workspace = true http.workspace = true mime_guess.workspace = true +rand.workspace = true reqwest.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/backend/windmill-ai/src/ai_bedrock.rs b/backend/windmill-ai/src/ai_bedrock.rs index a9318a1f3a..f55fcca415 100644 --- a/backend/windmill-ai/src/ai_bedrock.rs +++ b/backend/windmill-ai/src/ai_bedrock.rs @@ -477,6 +477,29 @@ pub async fn assume_role_with_oidc_token( // Error Formatting // ============================================================================ +/// The error for a Bedrock stream that failed after it opened, retrying the exceptions the +/// Vercel AI SDK retries (`amazon-bedrock-stream-error.ts`) and a broken connection. The +/// SDK's own retries cover the opening call alone. +pub fn bedrock_stream_error( + error: &aws_sdk_bedrockruntime::error::SdkError< + aws_sdk_bedrockruntime::types::error::ConverseStreamOutputError, + R, + >, +) -> Error { + let message = format!("Bedrock stream error: {}", format_bedrock_error(error)); + match error.as_service_error() { + Some(e) + if !(e.is_internal_server_exception() + || e.is_model_stream_error_exception() + || e.is_service_unavailable_exception() + || e.is_throttling_exception()) => + { + Error::AIError(message) + } + _ => crate::retry::transient_error(message, None), + } +} + /// Format AWS SDK errors with detailed information pub fn format_bedrock_error(error: &aws_sdk_bedrockruntime::error::SdkError) -> String where diff --git a/backend/windmill-ai/src/ai_google.rs b/backend/windmill-ai/src/ai_google.rs index bb8ae83e13..9f48edca8a 100644 --- a/backend/windmill-ai/src/ai_google.rs +++ b/backend/windmill-ai/src/ai_google.rs @@ -326,6 +326,17 @@ pub struct GeminiSSEEvent { pub candidates: Option>, #[serde(rename = "usageMetadata")] pub usage_metadata: Option, + #[serde(default)] + pub error: Option, + #[serde(rename = "promptFeedback", default)] + pub prompt_feedback: Option, +} + +/// Set when Gemini refuses the prompt itself, in which case no candidate follows. +#[derive(Deserialize, Debug)] +pub struct GeminiPromptFeedback { + #[serde(rename = "blockReason", default)] + pub block_reason: Option, } // ============================================================================ @@ -550,10 +561,40 @@ pub fn openai_tools_to_gemini( } } +/// Follows a Gemini stream to tell a complete turn from one cut short. +#[derive(Default)] +pub struct GeminiTurn { + ended: bool, + has_output: bool, +} + +impl GeminiTurn { + pub fn observe(&mut self, event: &GeminiParsedEvent) { + // A function call arrives whole and may come without a `finishReason`, so it + // counts too, as in gemini-cli's stream validation. + self.ended |= event.finish_reason.is_some() || !event.tool_calls.is_empty(); + self.has_output |= event.text.is_some() || event.reasoning.is_some(); + } + + /// Whether the turn's final event arrived. + pub fn ended(&self) -> bool { + self.ended + } + + /// Whether a stream that closed cleanly here holds the whole turn. Gemini 2.5 closes + /// its stream without a single event when thinking spends the whole + /// `maxOutputTokens`: an empty answer, which a retry would only get again, rather + /// than a cut one. + pub fn complete_at_close(&self) -> bool { + self.ended || !self.has_output + } +} + /// Parse one Gemini SSE data line into a [`GeminiParsedEvent`]. /// /// Returns `Ok(None)` for empty data or unrecognised payloads (e.g. `"[DONE]"`). /// Logs a warning and returns `Ok(None)` on JSON parse errors rather than propagating. +/// An error the stream reports is returned as one. pub fn parse_gemini_sse_event(data: &str) -> Result, Error> { if data.is_empty() || data == "[DONE]" { return Ok(None); @@ -567,6 +608,21 @@ pub fn parse_gemini_sse_event(data: &str) -> Result, E } }; + if let Some(error) = event.error { + return Err(error.into_error("Gemini")); + } + + // `BLOCK_REASON_UNSPECIFIED` is the protobuf enum's placeholder, not a block. + if let Some(reason) = event + .prompt_feedback + .and_then(|f| f.block_reason) + .filter(|r| !matches!(r.as_str(), "" | "BLOCK_REASON_UNSPECIFIED")) + { + return Err(Error::AIError(format!( + "Gemini blocked the prompt: {reason}" + ))); + } + let mut parsed = GeminiParsedEvent { usage: event.usage_metadata, ..Default::default() }; let Some(candidates) = event.candidates else { diff --git a/backend/windmill-ai/src/lib.rs b/backend/windmill-ai/src/lib.rs index ccc40da432..634614a3cf 100644 --- a/backend/windmill-ai/src/lib.rs +++ b/backend/windmill-ai/src/lib.rs @@ -11,6 +11,7 @@ pub mod model_context; pub mod providers; pub mod proxy; pub mod query_builder; +pub mod retry; pub mod sse; pub mod types; pub mod utils; diff --git a/backend/windmill-ai/src/providers/bedrock.rs b/backend/windmill-ai/src/providers/bedrock.rs index 836244a666..5aa142a127 100644 --- a/backend/windmill-ai/src/providers/bedrock.rs +++ b/backend/windmill-ai/src/providers/bedrock.rs @@ -9,11 +9,12 @@ use super::{anthropic_model_rejects_sampling_params, REASONING_OFF_SENTINEL}; use crate::{ ai_bedrock::{ - bedrock_model_supports_prompt_caching, bedrock_stream_event_is_block_stop, - bedrock_stream_event_to_reasoning_delta, bedrock_stream_event_to_text, - bedrock_stream_event_to_tool_delta, bedrock_stream_event_to_tool_delta_with_block_index, - bedrock_stream_event_to_tool_start, bedrock_stream_event_to_tool_start_with_block_index, - build_tool_config, create_inference_config, format_bedrock_error, json_to_document, + bedrock_model_supports_prompt_caching, bedrock_stream_error, + bedrock_stream_event_is_block_stop, bedrock_stream_event_to_reasoning_delta, + bedrock_stream_event_to_text, bedrock_stream_event_to_tool_delta, + bedrock_stream_event_to_tool_delta_with_block_index, bedrock_stream_event_to_tool_start, + bedrock_stream_event_to_tool_start_with_block_index, build_tool_config, + create_inference_config, format_bedrock_error, json_to_document, openai_messages_to_bedrock, streaming_tool_calls_to_openai, BearerTokenProvider, BedrockClient, StreamingToolCall, }, @@ -24,6 +25,7 @@ use crate::{ image_handler::prepare_messages_for_api, proxy::ProxyBuildArgs, query_builder::{ParsedResponse, StreamEventSink}, + retry::{openai_stream_error_chunk, truncated_stream_error}, types::{OpenAIMessage, StreamingEvent, TokenUsage, ToolDef}, }; use bytes::Bytes; @@ -461,26 +463,31 @@ pub fn sdk_stream_to_sse( async_stream::stream! { let mut stream = stream; let mut state = BedrockSseStreamState::new(id, model, created); + let mut message_stopped = false; - loop { + let failure = loop { match stream.recv().await { Ok(Some(event)) => { + if matches!( + event, + aws_sdk_bedrockruntime::types::ConverseStreamOutput::MessageStop(_) + ) { + message_stopped = true; + } for chunk in bedrock_sse_chunks_for_event(&event, &mut state) { yield Ok(chunk); } } - Ok(None) => break, - Err(e) => { - yield Err(std::io::Error::new( - std::io::ErrorKind::Other, - e.to_string(), - )); - break; - } + Ok(None) => break (!message_stopped).then(truncated_stream_error), + Err(_) if message_stopped => break None, + Err(e) => break Some(bedrock_stream_error(&e)), } - } + }; - yield Ok(Bytes::from("data: [DONE]\n\n")); + match failure { + Some(error) => yield Ok(Bytes::from(openai_stream_error_chunk(&error))), + None => yield Ok(Bytes::from("data: [DONE]\n\n")), + } } } @@ -1081,11 +1088,19 @@ impl BedrockQueryBuilder { // Claude reasoning block for the turn (only populated when thinking is on), // attached to the first tool call for replay before toolUse. let mut reasoning: Option = None; + let mut message_stopped = false; // Process stream events using shared parsing functions loop { match stream.recv().await { Ok(Some(event)) => { + if matches!( + event, + aws_sdk_bedrockruntime::types::ConverseStreamOutput::MessageStop(_) + ) { + message_stopped = true; + } + // Fold reasoning deltas into the turn's reasoning block (for // replay before toolUse, required by Claude when thinking is // on) and stream the readable summary as a thinking affordance. @@ -1161,11 +1176,13 @@ impl BedrockQueryBuilder { } } Ok(None) => break, - Err(e) => { - return Err(Error::internal_err(format!("Bedrock stream error: {}", e))); - } + Err(_) if message_stopped => break, + Err(e) => return Err(bedrock_stream_error(&e)), } } + if !message_stopped { + return Err(truncated_stream_error()); + } // Send tool call events to stream processor if let Some(processor) = stream_event_sink.as_ref() { diff --git a/backend/windmill-ai/src/providers/google_ai.rs b/backend/windmill-ai/src/providers/google_ai.rs index 97d0383ba4..bbf45e5e57 100644 --- a/backend/windmill-ai/src/providers/google_ai.rs +++ b/backend/windmill-ai/src/providers/google_ai.rs @@ -5,17 +5,20 @@ use crate::{ parse_gemini_response, parse_gemini_sse_event, sanitize_schema_for_google, GeminiFunctionDeclaration, GeminiGenerationConfig, GeminiImageContent, GeminiImageRequest, GeminiImageResponse, GeminiInlineData, GeminiPart, GeminiPredictContent, GeminiTextRequest, - GeminiThinkingConfig, GeminiTool, + GeminiThinkingConfig, GeminiTool, GeminiTurn, }, image_handler::{download_and_encode_s3_image, prepare_messages_for_api}, proxy::{ProxyBuildArgs, ProxyRequest}, query_builder::{BuildRequestArgs, ParsedResponse, QueryBuilder, StreamEventSink}, + retry::{ + openai_stream_error_chunk, send_with_retries, transient_error, truncated_stream_error, + }, sse::{GeminiSSEParser, SSEParser}, types::*, }; use async_trait::async_trait; use bytes::Bytes; -use eventsource_stream::Eventsource; +use eventsource_stream::{EventStreamError, Eventsource}; use futures::{stream::BoxStream, StreamExt}; use http::{header, HeaderMap, HeaderValue, Method, StatusCode}; use serde::Deserialize; @@ -336,9 +339,7 @@ pub async fn handle_google_ai_chat_proxy( ) -> Result { let GoogleAIProxyRequest { request, model, stream } = build_google_ai_chat_proxy_request(args)?; - let response = - send_google_ai_proxy_request(client, request, "Failed to send request to Gemini API") - .await?; + let response = send_google_ai_proxy_request(client, request).await?; if stream { Ok(convert_streaming_response(response, &model)) @@ -356,8 +357,7 @@ pub async fn handle_google_ai_models_proxy( args: &ProxyBuildArgs<'_>, ) -> Result { let request = build_google_ai_models_proxy_request(args); - let response = - send_google_ai_proxy_request(client, request, "Failed to fetch Gemini models").await?; + let response = send_google_ai_proxy_request(client, request).await?; let gemini_resp: GeminiModelsResponse = response.json().await.map_err(|e| { Error::internal_err(format!("Failed to parse Gemini models response: {}", e)) @@ -505,26 +505,13 @@ fn add_google_ai_auth_header(headers: &mut Vec<(String, String)>, api_key: &str, async fn send_google_ai_proxy_request( client: &reqwest::Client, proxy_request: ProxyRequest, - send_error_message: &str, ) -> Result { let mut request = client.request(proxy_request.method.clone(), &proxy_request.url); for (header_name, header_value) in &proxy_request.headers { request = request.header(header_name.as_str(), header_value.as_str()); } - let response = request - .body(proxy_request.body) - .send() - .await - .map_err(|e| Error::internal_err(format!("{}: {}", send_error_message, e)))?; - - if let Err(e) = response.error_for_status_ref() { - let status = e.status().map(|s| s.to_string()).unwrap_or_default(); - let body = response.text().await.unwrap_or_default(); - return Err(Error::AIError(format!("{}: {}", status, body))); - } - - Ok(response) + send_with_retries(request.body(proxy_request.body)).await } fn convert_streaming_response(response: reqwest::Response, model: &str) -> GoogleAIProxyResponse { @@ -535,10 +522,15 @@ fn convert_streaming_response(response: reqwest::Response, model: &str) -> Googl let openai_sse_stream = async_stream::stream! { tokio::pin!(gemini_sse_stream); let mut tool_call_index: usize = 0; - while let Some(event) = gemini_sse_stream.next().await { + let mut turn = GeminiTurn::default(); + let failure = loop { + let Some(event) = gemini_sse_stream.next().await else { + break (!turn.complete_at_close()).then(truncated_stream_error); + }; match event { Ok(event) => match parse_gemini_sse_event(&event.data) { Ok(Some(parsed)) => { + turn.observe(&parsed); for chunk in gemini_event_to_openai_sse_chunks( &parsed, &id, &model, &mut tool_call_index, ) { @@ -546,12 +538,23 @@ fn convert_streaming_response(response: reqwest::Response, model: &str) -> Googl } } Ok(None) => {} - Err(e) => tracing::error!("Error parsing Gemini SSE event: {}", e), + Err(e) => break Some(e), }, + Err(EventStreamError::Transport(_)) if turn.ended() => break None, + Err(EventStreamError::Transport(e)) => { + break Some(transient_error( + format!("The connection to Gemini broke off mid-response: {e}"), + None, + )); + } Err(e) => tracing::error!("Error reading Gemini SSE stream: {}", e), } - } - yield Ok::(Bytes::from("data: [DONE]\n\n")); + }; + let last = match failure { + Some(error) => openai_stream_error_chunk(&error), + None => "data: [DONE]\n\n".to_string(), + }; + yield Ok::(Bytes::from(last)); } .boxed(); diff --git a/backend/windmill-ai/src/providers/other.rs b/backend/windmill-ai/src/providers/other.rs index 1ea54a7e79..fea8edbdac 100644 --- a/backend/windmill-ai/src/providers/other.rs +++ b/backend/windmill-ai/src/providers/other.rs @@ -266,6 +266,7 @@ impl QueryBuilder for OtherQueryBuilder { mut events_str, stream_event_processor, usage: openai_usage, + .. } = openai_sse_parser; // Process streaming events with error handling diff --git a/backend/windmill-ai/src/retry.rs b/backend/windmill-ai/src/retry.rs new file mode 100644 index 0000000000..9de9d309da --- /dev/null +++ b/backend/windmill-ai/src/retry.rs @@ -0,0 +1,474 @@ +use std::time::Duration; + +use reqwest::{header::HeaderMap, RequestBuilder, Response, StatusCode}; +use windmill_common::error::{to_anyhow, Error}; + +/// How many times a provider call is retried after its first attempt. +pub const MAX_RETRIES: u32 = 3; +const BASE_DELAY: Duration = Duration::from_secs(1); +const MAX_BACKOFF: Duration = Duration::from_secs(10); +/// A provider that asks for a longer wait is out of capacity for longer than anyone should +/// block on (a spent daily quota, say), so its error is returned instead of waited out. +const MAX_RETRY_AFTER: Duration = Duration::from_secs(60); + +/// A provider call that failed for a reason a later attempt may not meet: the provider was +/// overloaded, throttled or unreachable, or its response broke off part way. +#[derive(Debug)] +pub struct TransientAIError { + pub message: String, + /// The wait the provider asked for, when it named one. + pub retry_after: Option, +} + +impl std::fmt::Display for TransientAIError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.message) + } +} + +impl std::error::Error for TransientAIError {} + +pub fn transient_error(message: impl Into, retry_after: Option) -> Error { + to_anyhow(TransientAIError { message: message.into(), retry_after }).into() +} + +pub fn as_transient(error: &Error) -> Option<&TransientAIError> { + match error { + Error::Anyhow { error, .. } => error.downcast_ref::(), + _ => None, + } +} + +/// The error to report once a transient failure is no longer retried, without the source +/// location `Error::Anyhow` renders. +pub fn into_final_error(error: Error) -> Error { + match as_transient(&error) { + Some(transient) => Error::AIError(transient.message.clone()), + None => error, + } +} + +/// The statuses the OpenAI and Anthropic SDKs and the Vercel AI SDK retry. +fn is_retryable_status(status: u16) -> bool { + matches!(status, 408 | 409 | 429 | 500..=599) +} + +/// The error for a provider's error response. The rules follow the OpenAI and Anthropic +/// SDKs (`x-should-retry`, then the status) and gemini-cli (a daily quota is spent until +/// tomorrow, and the wait Gemini asks for is in its body). +pub fn response_error( + status: StatusCode, + headers: &HeaderMap, + body: &str, + message: String, +) -> Error { + let retryable = match headers.get("x-should-retry").and_then(|v| v.to_str().ok()) { + Some("true") => true, + Some("false") => false, + _ => { + is_retryable_status(status.as_u16()) + // OpenAI answers a spent balance with the 429 it also uses for rate limits. + && !body.contains("insufficient_quota") + && !google_quota_spent_for_the_day(body) + } + }; + if retryable { + transient_error( + message, + retry_after(headers).or_else(|| google_retry_delay(body)), + ) + } else { + Error::AIError(message) + } +} + +/// The `details` of a Google API error body, where Gemini puts its quota and retry hints. +fn google_error_details(body: &str) -> Vec { + serde_json::from_str::(body) + .ok() + .and_then(|v| v.get("error")?.get("details")?.as_array().cloned()) + .unwrap_or_default() +} + +fn google_quota_spent_for_the_day(body: &str) -> bool { + google_error_details(body).iter().any(|detail| { + detail + .get("violations") + .and_then(|v| v.as_array()) + .is_some_and(|violations| { + violations.iter().any(|violation| { + violation + .get("quotaId") + .and_then(|id| id.as_str()) + .is_some_and(|id| id.contains("PerDay") || id.contains("Daily")) + }) + }) + }) +} + +/// The `RetryInfo.retryDelay` of a Google API error, a protobuf duration such as `"53s"`. +fn google_retry_delay(body: &str) -> Option { + google_error_details(body).iter().find_map(|detail| { + let delay = detail.get("retryDelay")?.as_str()?.strip_suffix('s')?; + let seconds = delay + .parse::() + .ok() + .filter(|v| v.is_finite() && *v >= 0.0)?; + Some(Duration::try_from_secs_f64(seconds).unwrap_or(Duration::MAX)) + }) +} + +/// The wait a provider asked for in its `retry-after-ms` or `retry-after` header. An +/// HTTP-date `retry-after` is ignored, which falls back to the computed backoff. +fn retry_after(headers: &HeaderMap) -> Option { + let seconds = |name: &str, scale: f64| { + headers + .get(name)? + .to_str() + .ok()? + .trim() + .parse::() + .ok() + .filter(|v| v.is_finite() && *v >= 0.0) + .map(|v| Duration::try_from_secs_f64(v / scale).unwrap_or(Duration::MAX)) + }; + seconds("retry-after-ms", 1000.0).or_else(|| seconds("retry-after", 1.0)) +} + +/// The error object a provider sends inside a stream: OpenAI-compatible chunks carry +/// `type` and a string or status `code`, Anthropic `type`, Gemini a status `code` and +/// `status`. +#[derive(Debug, Default, serde::Deserialize)] +pub struct StreamErrorBody { + #[serde(default)] + pub message: Option, + #[serde(default, rename = "type")] + pub kind: Option, + #[serde(default)] + pub code: Option, + #[serde(default)] + pub status: Option, +} + +impl StreamErrorBody { + /// The error for a failure an OpenAI-compatible or Gemini stream reported after opening + /// with a 200, classified as the Vercel AI SDK classifies OpenAI stream errors + /// (`openai-stream-error.ts`): the status `code` holds, else the status its code and type + /// name, else a server error. + pub fn into_error(self, provider: &str) -> Error { + let code = self.code_str(); + let discriminator = [code.as_deref(), self.kind.as_deref()] + .into_iter() + .flatten() + .collect::>() + .join(" ") + .to_lowercase(); + let has = |terms: &[&str]| terms.iter().any(|term| discriminator.contains(term)); + let status = match code.as_deref().and_then(|code| code.parse::().ok()) { + Some(status @ 400..=599) => status, + _ if has(&["insufficient_quota", "rate_limit"]) => 429, + _ if has(&["authentication"]) => 401, + _ if has(&["permission"]) => 403, + _ if has(&["not_found"]) => 404, + _ if has(&["invalid", "bad_request", "context_length"]) => 400, + _ if has(&["overload"]) => 503, + _ if has(&["timeout"]) => 504, + _ => 500, + }; + let retryable = !has(&["insufficient_quota"]) && is_retryable_status(status); + self.into_classified_error(provider, retryable) + } + + /// The error for a failure an Anthropic stream reported after opening with a 200. The + /// kinds retried are those the Vercel AI SDK retries, plus `timeout_error`, which + /// Anthropic documents as its 504; any other kind is a fault in the request or account. + pub fn into_anthropic_error(self) -> Error { + let retryable = matches!( + self.kind.as_deref(), + Some("api_error" | "overloaded_error" | "rate_limit_error" | "timeout_error") + ); + self.into_classified_error("Anthropic", retryable) + } + + fn code_str(&self) -> Option { + match self.code.as_ref()? { + serde_json::Value::String(code) => Some(code.clone()), + serde_json::Value::Number(code) => Some(code.to_string()), + _ => None, + } + } + + fn into_classified_error(self, provider: &str, retryable: bool) -> Error { + let kind = [self.kind.clone(), self.code_str(), self.status] + .into_iter() + .flatten() + .collect::>() + .join("/"); + let message = format!( + "{provider} stream error{}: {}", + (!kind.is_empty()) + .then(|| format!(" ({kind})")) + .unwrap_or_default(), + self.message.as_deref().unwrap_or("no details") + ); + if retryable { + transient_error(message, None) + } else { + Error::AIError(message) + } + } +} + +/// The error for a stream that closed before the provider sent the event ending a +/// complete response: cut off by the provider or by a proxy in between. +pub fn truncated_stream_error() -> Error { + transient_error( + "The AI provider's response stream ended before the response was complete", + None, + ) +} + +/// An OpenAI-format stream chunk reporting `error`, which ends a response converted to that +/// format after it started streaming: the chat's OpenAI SDK throws on a chunk carrying +/// `error`, where a bare end of stream would read as a finished answer. +pub fn openai_stream_error_chunk(error: &Error) -> String { + let message = as_transient(error).map_or_else(|| error.to_string(), |t| t.message.clone()); + format!( + "data: {}\n\n", + serde_json::json!({ "error": { "message": message } }) + ) +} + +/// Sends a provider request, retrying a transient failure while nothing of the response +/// has been read: a failed connection or an error status. Once retries are spent the error +/// carries the provider's answer, as a single attempt's would. +pub async fn send_with_retries(request: RequestBuilder) -> Result { + let mut backoff = Backoff::default(); + loop { + // A streamed body cannot be replayed, so such a request is sent once. + let Some(attempt) = request.try_clone() else { + return send_once(request).await.map_err(into_final_error); + }; + let error = match send_once(attempt).await { + Ok(response) => return Ok(response), + Err(error) => error, + }; + let Some(delay) = as_transient(&error).and_then(|t| backoff.next_delay(t)) else { + return Err(into_final_error(error)); + }; + tracing::warn!( + "AI provider call failed ({error}); retrying in {:.1}s ({}/{MAX_RETRIES})", + delay.as_secs_f64(), + backoff.retries() + ); + tokio::time::sleep(delay).await; + } +} + +/// The error for a provider request that failed before any response arrived. The request +/// timeout is the caller's whole budget, so a timed-out call leaves no time for another +/// attempt; a connect timeout has its own, shorter one. +pub fn send_error(e: reqwest::Error, context: &str) -> Error { + let message = format!("{context}: {e}"); + if e.is_builder() || (e.is_timeout() && !e.is_connect()) { + Error::AIError(message) + } else { + transient_error(message, None) + } +} + +async fn send_once(request: RequestBuilder) -> Result { + let response = request + .send() + .await + .map_err(|e| send_error(e, "Failed to reach the AI provider"))?; + let status = response.status(); + if status.is_client_error() || status.is_server_error() { + let headers = response.headers().clone(); + let body = response.text().await.unwrap_or_default(); + return Err(response_error(status, &headers, &body, body.clone())); + } + Ok(response) +} + +/// Counts the retries of one provider call and spaces them out. +#[derive(Debug, Default)] +pub struct Backoff { + retries: u32, +} + +impl Backoff { + /// The wait before retrying a call that failed with `error`, or `None` once it should + /// fail instead. + pub fn next_delay(&mut self, error: &TransientAIError) -> Option { + if self.retries >= MAX_RETRIES { + return None; + } + let delay = match error.retry_after { + Some(wait) if wait > MAX_RETRY_AFTER => return None, + // A zero wait would retry back to back, so it gets the computed backoff. + Some(wait) if !wait.is_zero() => wait, + // Jittered, so callers throttled together do not come back together. + _ => (BASE_DELAY * 2u32.pow(self.retries)) + .min(MAX_BACKOFF) + .mul_f64(0.5 + rand::random::() * 0.5), + }; + self.retries += 1; + Some(delay) + } + + /// Retries granted so far. + pub fn retries(&self) -> u32 { + self.retries + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Error responses as providers send them, each with the retry decision of the client + /// the rule is taken from. + #[test] + fn classifies_error_responses() { + const GEMINI_QUOTA: &str = r#"{"error":{"code":429,"status":"RESOURCE_EXHAUSTED","details":[{"@type":"type.googleapis.com/google.rpc.QuotaFailure","violations":[{"quotaId":"QUOTA_ID"}]},{"@type":"type.googleapis.com/google.rpc.RetryInfo","retryDelay":"53s"}]}}"#; + let cases: &[(u16, &[(&str, &str)], &str, Option>)] = &[ + ( + 429, + &[], + r#"{"error":{"code":"rate_limit_exceeded"}}"#, + Some(None), + ), + ( + 429, + &[], + r#"{"error":{"type":"insufficient_quota","code":"insufficient_quota"}}"#, + None, + ), + ( + 529, + &[], + r#"{"type":"error","error":{"type":"overloaded_error"}}"#, + Some(None), + ), + (501, &[], "", Some(None)), + (400, &[], "", None), + (401, &[], "", None), + (400, &[("x-should-retry", "true")], "", Some(None)), + (503, &[("x-should-retry", "false")], "", None), + (429, &[("retry-after", "2")], "", Some(Some(2))), + ( + 429, + &[], + &GEMINI_QUOTA.replace("QUOTA_ID", "GenerateRequestsPerMinutePerProjectPerModel"), + Some(Some(53)), + ), + ( + 429, + &[], + &GEMINI_QUOTA.replace( + "QUOTA_ID", + "GenerateRequestsPerDayPerProjectPerModel-FreeTier", + ), + None, + ), + ]; + for (status, headers, body, expected) in cases { + let headers = headers + .iter() + .map(|(k, v)| (k.parse().unwrap(), v.parse().unwrap())) + .collect::(); + let error = response_error( + StatusCode::from_u16(*status).unwrap(), + &headers, + body, + String::new(), + ); + let got = as_transient(&error).map(|t| t.retry_after.map(|d| d.as_secs())); + assert_eq!(&got, expected, "{status} {headers:?} {body}"); + } + } + + /// In-stream error objects as providers send them, after the 200 opened the stream. + #[test] + fn classifies_stream_errors() { + let retried = |json: &str, anthropic: bool| { + let body: StreamErrorBody = serde_json::from_str(json).unwrap(); + let error = if anthropic { + body.into_anthropic_error() + } else { + body.into_error("AI provider") + }; + as_transient(&error).is_some() + }; + for (json, expected) in [ + (r#"{"message":"m","type":"server_error"}"#, true), + ( + r#"{"message":"m","type":"requests","code":"rate_limit_exceeded"}"#, + true, + ), + ( + r#"{"message":"m","type":"insufficient_quota","code":"insufficient_quota"}"#, + false, + ), + ( + r#"{"message":"m","type":"invalid_request_error","code":"context_length_exceeded"}"#, + false, + ), + // LiteLLM's proxy puts the upstream status in `code` as a string. + (r#"{"message":"m","type":"None","code":"400"}"#, false), + (r#"{"message":"m","code":"503"}"#, true), + (r#"{"code":503,"message":"m","status":"UNAVAILABLE"}"#, true), + ( + r#"{"code":400,"message":"m","status":"INVALID_ARGUMENT"}"#, + false, + ), + (r#"{"message":"m"}"#, true), + ] { + assert_eq!(retried(json, false), expected, "{json}"); + } + for (kind, expected) in [ + ("overloaded_error", true), + ("api_error", true), + ("rate_limit_error", true), + ("timeout_error", true), + ("billing_error", false), + ("invalid_request_error", false), + ("some_new_error", false), + ] { + let json = format!(r#"{{"type":"{kind}","message":"m"}}"#); + assert_eq!(retried(&json, true), expected, "{kind}"); + } + } + + #[test] + fn honours_retry_after_up_to_a_bound() { + let mut headers = HeaderMap::new(); + headers.insert("retry-after", "2".parse().unwrap()); + assert_eq!(retry_after(&headers), Some(Duration::from_secs(2))); + headers.insert("retry-after-ms", "1500".parse().unwrap()); + assert_eq!(retry_after(&headers), Some(Duration::from_millis(1500))); + headers.remove("retry-after-ms"); + headers.insert("retry-after", "1e30".parse().unwrap()); + assert_eq!(retry_after(&headers), Some(Duration::MAX)); + + let mut backoff = Backoff::default(); + let asked = |secs| TransientAIError { + message: String::new(), + retry_after: Some(Duration::from_secs(secs)), + }; + assert_eq!(backoff.next_delay(&asked(5)), Some(Duration::from_secs(5))); + assert_eq!(backoff.next_delay(&asked(3600)), None); + } + + #[test] + fn stops_after_max_retries() { + let mut backoff = Backoff::default(); + let error = TransientAIError { message: String::new(), retry_after: None }; + for _ in 0..MAX_RETRIES { + let delay = backoff.next_delay(&error).expect("retry"); + assert!(delay <= MAX_BACKOFF); + } + assert_eq!(backoff.next_delay(&error), None); + } +} diff --git a/backend/windmill-ai/src/sse.rs b/backend/windmill-ai/src/sse.rs index ada50afca5..bdaa75025f 100644 --- a/backend/windmill-ai/src/sse.rs +++ b/backend/windmill-ai/src/sse.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use eventsource_stream::Eventsource; +use eventsource_stream::{EventStreamError, Eventsource}; use indexmap::IndexMap; use reqwest::Response; use serde::Deserialize; @@ -8,12 +8,13 @@ use tokio_stream::StreamExt; use windmill_common::{error::Error, utils::rd_string}; use crate::{ - ai_google::{parse_gemini_sse_event, GeminiUsageMetadata}, + ai_google::{parse_gemini_sse_event, GeminiTurn, GeminiUsageMetadata}, ai_types::UrlCitation, ai_types::{ AnthropicExtraContent, ExtraContent, GoogleExtraContent, OpenAIFunction, OpenAIToolCall, }, query_builder::StreamEventSink, + retry::{transient_error, truncated_stream_error, StreamErrorBody}, types::{StreamingEvent, TokenUsage}, }; @@ -43,6 +44,8 @@ pub struct OpenAIChoiceDelta { #[derive(Deserialize)] pub struct OpenAIChoice { pub delta: Option, + #[serde(default)] + pub finish_reason: Option, } /// Nested prompt token details returned by the Chat Completions API. @@ -87,6 +90,8 @@ pub struct OpenAISSEEvent { pub choices: Option>, #[serde(default)] pub usage: Option, + #[serde(default)] + pub error: Option, } lazy_static::lazy_static! { @@ -99,6 +104,14 @@ lazy_static::lazy_static! { pub trait SSEParser { async fn parse_event_data(&mut self, data: &str) -> Result<(), Error>; + /// Whether the stream delivered the event that ends a complete response. + fn is_complete(&self) -> bool; + + /// Whether a stream that closed cleanly holds a complete response. + fn is_complete_at_close(&self) -> bool { + self.is_complete() + } + async fn parse_events(&mut self, response: Response) -> Result<(), Error> { let mut stream = response.bytes_stream().eventsource(); let mut consecutive_errors = 0; @@ -113,6 +126,18 @@ pub trait SSEParser { self.parse_event_data(&event.data).await?; } + // The body ends right after a transport error, so it cannot be skipped + // like a malformed event. A timeout spent the caller's whole budget. + Err(EventStreamError::Transport(_)) if self.is_complete() => break, + Err(EventStreamError::Transport(e)) => { + let message = + format!("The connection to the AI provider broke off mid-response: {e}"); + return Err(if e.is_timeout() { + Error::AIError(message) + } else { + transient_error(message, None) + }); + } Err(e) => { consecutive_errors += 1; tracing::error!("Failed to parse SSE event: {}", e); @@ -130,6 +155,9 @@ pub trait SSEParser { } } + if !self.is_complete_at_close() { + return Err(truncated_stream_error()); + } Ok(()) } } @@ -145,6 +173,9 @@ pub struct OpenAISSEParser { pub stream_event_processor: Box, /// Token usage from final chunk (when stream_options.include_usage is true) pub usage: Option, + /// Set by `[DONE]` or by a choice's `finish_reason`: OpenAI-compatible gateways do not + /// all send `[DONE]`. + complete: bool, } impl OpenAISSEParser { @@ -156,13 +187,19 @@ impl OpenAISSEParser { events_str: String::new(), stream_event_processor, usage: None, + complete: false, } } } impl SSEParser for OpenAISSEParser { + fn is_complete(&self) -> bool { + self.complete + } + async fn parse_event_data(&mut self, data: &str) -> Result<(), Error> { if data == "[DONE]" { + self.complete = true; return Ok(()); } @@ -173,13 +210,21 @@ impl SSEParser for OpenAISSEParser { .ok(); if let Some(event) = event { + if let Some(error) = event.error { + return Err(error.into_error("AI provider")); + } + // Extract usage from final chunk (when stream_options.include_usage is true) if let Some(usage) = event.usage { self.usage = Some(usage); } if let Some(mut choices) = event.choices.filter(|s| !s.is_empty()) { - if let Some(delta) = choices.remove(0).delta { + let choice = choices.remove(0); + if choice.finish_reason.is_some() { + self.complete = true; + } + if let Some(delta) = choice.delta { if let Some(reasoning) = delta.reasoning_content.filter(|s| !s.is_empty()) { self.accumulated_reasoning.push_str(&reasoning); let event = StreamingEvent::ReasoningTokenDelta { content: reasoning }; @@ -345,6 +390,8 @@ pub enum AnthropicSSEEvent { MessageDelta { #[serde(default)] usage: Option, + #[serde(default)] + delta: Option, }, #[serde(rename = "message_stop")] MessageStop {}, @@ -353,12 +400,18 @@ pub enum AnthropicSSEEvent { #[serde(rename = "error")] Error { #[serde(default)] - message: Option, + error: StreamErrorBody, }, #[serde(other)] Unknown, } +#[derive(Deserialize, Debug)] +pub struct AnthropicMessageDelta { + #[serde(default)] + pub stop_reason: Option, +} + /// Tracks state of a content block during streaming #[derive(Debug)] #[allow(dead_code)] @@ -391,6 +444,9 @@ pub struct AnthropicSSEParser { /// can be replayed before `tool_use` (required by Claude when thinking is on). pending_reasoning: Option, reasoning_attached: bool, + /// Set by `message_stop` or by the `stop_reason` of the `message_delta` sent just + /// before it, so a gateway that drops `message_stop` does not fail a whole answer. + complete: bool, } impl AnthropicSSEParser { @@ -407,11 +463,16 @@ impl AnthropicSSEParser { usage: None, pending_reasoning: None, reasoning_attached: false, + complete: false, } } } impl SSEParser for AnthropicSSEParser { + fn is_complete(&self) -> bool { + self.complete + } + async fn parse_event_data(&mut self, data: &str) -> Result<(), Error> { let event: Option = serde_json::from_str(data) .inspect_err(|e| { @@ -583,9 +644,8 @@ impl SSEParser for AnthropicSSEParser { .await?; } } - AnthropicSSEEvent::Error { message } => { - let error_msg = message.unwrap_or_else(|| "Unknown error".to_string()); - tracing::error!("Anthropic streaming error: {}", error_msg); + AnthropicSSEEvent::Error { error } => { + return Err(error.into_anthropic_error()); } AnthropicSSEEvent::MessageStart { message } => { // The only event carrying the prompt-side counts. `message_delta` @@ -595,7 +655,10 @@ impl SSEParser for AnthropicSSEParser { self.usage = Some(usage); } } - AnthropicSSEEvent::MessageDelta { usage } => { + AnthropicSSEEvent::MessageDelta { usage, delta } => { + if delta.is_some_and(|d| d.stop_reason.is_some()) { + self.complete = true; + } if let Some(u) = usage { match &mut self.usage { // Field by field, so the prompt counts from `message_start` @@ -614,10 +677,9 @@ impl SSEParser for AnthropicSSEParser { } } } + AnthropicSSEEvent::MessageStop {} => self.complete = true, // Ignore other events - AnthropicSSEEvent::MessageStop {} - | AnthropicSSEEvent::Ping {} - | AnthropicSSEEvent::Unknown => {} + AnthropicSSEEvent::Ping {} | AnthropicSSEEvent::Unknown => {} } } @@ -645,6 +707,7 @@ pub struct GeminiSSEParser { pub annotations: Vec, pub used_websearch: bool, pub usage: Option, + turn: GeminiTurn, } impl GeminiSSEParser { @@ -659,15 +722,25 @@ impl GeminiSSEParser { annotations: Vec::new(), used_websearch: false, usage: None, + turn: GeminiTurn::default(), } } } impl SSEParser for GeminiSSEParser { + fn is_complete(&self) -> bool { + self.turn.ended() + } + + fn is_complete_at_close(&self) -> bool { + self.turn.complete_at_close() + } + async fn parse_event_data(&mut self, data: &str) -> Result<(), Error> { let Some(parsed) = parse_gemini_sse_event(data)? else { return Ok(()); }; + self.turn.observe(&parsed); if let Some(reasoning) = parsed.reasoning.filter(|s| !s.is_empty()) { self.accumulated_reasoning.push_str(&reasoning); @@ -797,6 +870,8 @@ impl OpenAIResponsesUsage { pub struct OpenAIResponsesResponse { #[serde(default)] pub usage: Option, + #[serde(default)] + pub error: Option, } /// SSE event types for OpenAI Responses API streaming @@ -833,6 +908,20 @@ pub enum OpenAIResponsesSSEEvent { #[serde(rename = "response.incomplete")] Incomplete { response: OpenAIResponsesResponse }, + #[serde(rename = "response.failed")] + Failed { response: OpenAIResponsesResponse }, + + /// Flat (`code`, `message`) from OpenAI, nested under `error` from some compatible servers. + #[serde(rename = "error")] + Error { + #[serde(default)] + code: Option, + #[serde(default)] + message: Option, + #[serde(default)] + error: Option, + }, + /// Response created #[serde(rename = "response.created")] Created {}, @@ -890,6 +979,7 @@ pub struct OpenAIResponsesSSEParser { pub usage: Option, /// Reasoning summary parts seen so far, to separate them as paragraphs reasoning_summary_parts: usize, + response_ended: bool, } impl OpenAIResponsesSSEParser { @@ -906,11 +996,16 @@ impl OpenAIResponsesSSEParser { used_websearch: false, usage: None, reasoning_summary_parts: 0, + response_ended: false, } } } impl SSEParser for OpenAIResponsesSSEParser { + fn is_complete(&self) -> bool { + self.response_ended + } + async fn parse_event_data(&mut self, data: &str) -> Result<(), Error> { let event: Option = serde_json::from_str(data) .inspect_err(|e| { @@ -1010,11 +1105,22 @@ impl SSEParser for OpenAIResponsesSSEParser { OpenAIResponsesSSEEvent::Completed { response } | OpenAIResponsesSSEEvent::Incomplete { response } => { + self.response_ended = true; if let Some(usage) = response.usage { self.usage = Some(usage); } } + OpenAIResponsesSSEEvent::Failed { response } => { + return Err(response.error.unwrap_or_default().into_error("OpenAI")); + } + + OpenAIResponsesSSEEvent::Error { code, message, error } => { + return Err(error + .unwrap_or(StreamErrorBody { code, message, ..Default::default() }) + .into_error("OpenAI")); + } + OpenAIResponsesSSEEvent::ReasoningSummaryPartAdded {} => { self.reasoning_summary_parts += 1; if self.reasoning_summary_parts > 1 { @@ -1037,9 +1143,10 @@ impl SSEParser for OpenAIResponsesSSEParser { } } + OpenAIResponsesSSEEvent::Done {} => self.response_ended = true, + // Ignore other event types - OpenAIResponsesSSEEvent::Done {} - | OpenAIResponsesSSEEvent::Created {} + OpenAIResponsesSSEEvent::Created {} | OpenAIResponsesSSEEvent::InProgress {} | OpenAIResponsesSSEEvent::OutputItemDone {} | OpenAIResponsesSSEEvent::ContentPartAdded {} @@ -1093,6 +1200,117 @@ mod tests { } } + fn sse_response(chunks: Vec>) -> Response { + let body = reqwest::Body::wrap_stream(futures::stream::iter(chunks)); + Response::from(http::Response::new(body)) + } + + /// A stream that breaks off, or reports a failure inside its 200, must fail the call + /// rather than hand back what arrived as if it were the whole answer. + #[tokio::test] + async fn stream_failures_are_errors_not_partial_answers() { + use crate::retry::as_transient; + + const STARTED: &str = concat!( + "data: {\"type\":\"message_start\"}\n\n", + "data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n", + "data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Hel\"}}\n\n", + ); + let anthropic = |chunks| async move { + AnthropicSSEParser::new(Box::new(NoopSink)) + .parse_events(sse_response(chunks)) + .await + }; + + let overloaded = anthropic(vec![ + Ok(STARTED), + Ok("data: {\"type\":\"error\",\"error\":{\"type\":\"overloaded_error\",\"message\":\"Overloaded\"}}\n\n"), + ]) + .await + .unwrap_err(); + assert!(as_transient(&overloaded).is_some(), "{overloaded}"); + + let refused = anthropic(vec![ + Ok(STARTED), + Ok("data: {\"type\":\"error\",\"error\":{\"type\":\"invalid_request_error\",\"message\":\"bad\"}}\n\n"), + ]) + .await + .unwrap_err(); + assert!(as_transient(&refused).is_none(), "{refused}"); + + let truncated = anthropic(vec![Ok(STARTED)]).await.unwrap_err(); + assert!(as_transient(&truncated).is_some(), "{truncated}"); + + let dropped = anthropic(vec![ + Ok(STARTED), + Err(std::io::Error::new( + std::io::ErrorKind::ConnectionReset, + "reset", + )), + ]) + .await + .unwrap_err(); + assert!(as_transient(&dropped).is_some(), "{dropped}"); + + anthropic(vec![ + Ok(STARTED), + Ok("data: {\"type\":\"message_stop\"}\n\n"), + ]) + .await + .unwrap(); + + anthropic(vec![ + Ok(STARTED), + Ok("data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"}}\n\n"), + ]) + .await + .unwrap(); + + let truncated = OpenAISSEParser::new(Box::new(NoopSink)) + .parse_events(sse_response(vec![Ok( + "data: {\"choices\":[{\"delta\":{\"content\":\"Hi\"}}]}\n\n", + )])) + .await + .unwrap_err(); + assert!(as_transient(&truncated).is_some(), "{truncated}"); + + let quota = OpenAIResponsesSSEParser::new(Box::new(NoopSink)) + .parse_events(sse_response(vec![Ok( + "data: {\"type\":\"error\",\"error\":{\"type\":\"insufficient_quota\",\"code\":\"insufficient_quota\",\"message\":\"m\"}}\n\n", + )])) + .await + .unwrap_err(); + assert!(as_transient("a).is_none(), "{quota}"); + + GeminiSSEParser::new(Box::new(NoopSink)) + .parse_events(sse_response(vec![Ok( + "data: {\"candidates\":[{\"content\":{\"role\":\"model\",\"parts\":[{\"functionCall\":{\"name\":\"get_weather\",\"args\":{}}}]}}]}\n\n", + )])) + .await + .unwrap(); + + let gemini = |chunks| async move { + GeminiSSEParser::new(Box::new(NoopSink)) + .parse_events(sse_response(chunks)) + .await + }; + // Thinking spent the whole `maxOutputTokens`: a clean close with no event. + gemini(vec![]).await.unwrap(); + let dropped = gemini(vec![Err(std::io::Error::new( + std::io::ErrorKind::ConnectionReset, + "reset", + ))]) + .await + .unwrap_err(); + assert!(as_transient(&dropped).is_some(), "{dropped}"); + let truncated = gemini(vec![Ok( + "data: {\"candidates\":[{\"content\":{\"role\":\"model\",\"parts\":[{\"text\":\"Hi\"}]}}]}\n\n", + )]) + .await + .unwrap_err(); + assert!(as_transient(&truncated).is_some(), "{truncated}"); + } + /// The prompt-side counts arrive only on `message_start` and the completion total /// only on `message_delta`; a parser that keeps just the last one reports a request /// with no input tokens at all. diff --git a/backend/windmill-api/src/ai.rs b/backend/windmill-api/src/ai.rs index c401568868..cf7447ceba 100644 --- a/backend/windmill-api/src/ai.rs +++ b/backend/windmill-api/src/ai.rs @@ -40,6 +40,7 @@ use windmill_ai::proxy::{ fim::maybe_transform_fim_request, proxy_execution_mode, ProxyBuildArgs, ProxyExecutionMode, ProxyRequest, }; +use windmill_ai::retry::send_with_retries; use windmill_audit::{audit_oss::audit_log, ActionKind}; use windmill_common::db::UserDB; use windmill_common::error::{to_anyhow, Error, Result}; @@ -1188,15 +1189,11 @@ async fn global_proxy( } }; - let response = request.send().await.map_err(to_anyhow)?; + let response = send_with_retries(request).await; audit_global_ai_request(&db, &authed).await?; - if response.error_for_status_ref().is_err() { - let err_msg = response.text().await.unwrap_or("".to_string()); - return Err(Error::AIError(err_msg)); - } - + let response = response?; let status_code = response.status(); let headers = response.headers().clone(); let stream = response.bytes_stream(); @@ -1576,7 +1573,7 @@ async fn proxy( } }; - let response = request.send().await.map_err(to_anyhow)?; + let response = send_with_retries(request).await; let mut tx = db.begin().await?; @@ -1592,11 +1589,7 @@ async fn proxy( .await?; tx.commit().await?; - if response.error_for_status_ref().is_err() { - let err_msg = response.text().await.unwrap_or("".to_string()); - return Err(Error::AIError(err_msg)); - } - + let response = response?; let status_code = response.status(); let headers = response.headers().clone(); let is_sse = is_sse_response(&headers); diff --git a/backend/windmill-worker/src/ai/stream_event_processor.rs b/backend/windmill-worker/src/ai/stream_event_processor.rs index fe8a3b9b00..c4d17f080a 100644 --- a/backend/windmill-worker/src/ai/stream_event_processor.rs +++ b/backend/windmill-worker/src/ai/stream_event_processor.rs @@ -1,3 +1,8 @@ +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, +}; + use async_trait::async_trait; use windmill_ai::{query_builder::StreamEventSink, types::*}; use windmill_common::{error::Error, worker::Connection}; @@ -10,11 +15,13 @@ use crate::job_logger::append_result_stream; pub struct StreamEventProcessor { tx: Option>, pub handle: Option>, + /// Shared by every clone, so a sink lent to a parser counts into its owner. + streamed: Arc, } impl Clone for StreamEventProcessor { fn clone(&self) -> Self { - Self { tx: self.tx.clone(), handle: None } + Self { tx: self.tx.clone(), handle: None, streamed: self.streamed.clone() } } } @@ -48,13 +55,19 @@ impl StreamEventProcessor { } }); - Self { tx: Some(tx), handle: Some(handle) } + Self { tx: Some(tx), handle: Some(handle), streamed: Arc::default() } } /// Create a silent StreamEventProcessor that only accumulates events locally /// without persisting them to the database. Used when streaming is disabled. pub fn new_silent() -> Self { - Self { tx: None, handle: None } + Self { tx: None, handle: None, streamed: Arc::default() } + } + + /// How many events have been streamed to the job's readers. A silent processor + /// streams none. + pub fn streamed_events(&self) -> usize { + self.streamed.load(Ordering::Relaxed) } pub fn to_handle(self) -> Option> { @@ -79,6 +92,7 @@ impl StreamEventSink for StreamEventProcessor { Ok(event_json) => { let event_json = format!("{}\n", event_json); events_str.push_str(&event_json); + self.streamed.fetch_add(1, Ordering::Relaxed); if let Err(err) = tx .send(event_json.clone()) diff --git a/backend/windmill-worker/src/ai_executor.rs b/backend/windmill-worker/src/ai_executor.rs index 7dd2382731..1b889aea34 100644 --- a/backend/windmill-worker/src/ai_executor.rs +++ b/backend/windmill-worker/src/ai_executor.rs @@ -43,6 +43,7 @@ use windmill_ai::{ common_outbound_headers, needs_unavailable_oauth_exchange, retain_effective_credentials, }, query_builder::{BuildRequestArgs, ParsedResponse}, + retry::{as_transient, into_final_error, response_error, send_error, Backoff, MAX_RETRIES}, types::*, utils::{pinned_ai_client_for, should_use_structured_output_tool}, }; @@ -1659,10 +1660,17 @@ pub async fn run_agent( .await; let mut retried_context = false; + let mut backoff = Backoff::default(); + let streamed_events = || { + stream_event_processor + .as_ref() + .map_or(0, |processor| processor.streamed_events()) + }; let (parsed, request_message_count) = loop { // How many messages this request carries, so the provider's prompt count can // later be told apart from what the response and its tool results add. let request_message_count = messages.context().len(); + let streamed_before = streamed_events(); // Handle AWS Bedrock provider specially using the official SDK let attempt: error::Result<_> = async { @@ -1818,7 +1826,7 @@ pub async fn run_agent( let resp = build_http_request(&endpoint, &auth_headers, request_body) .send() .await - .map_err(|e| Error::internal_err(format!("Failed to call API: {}", e)))?; + .map_err(|e| send_error(e, "Failed to call API"))?; match resp.error_for_status_ref() { Ok(_) => { @@ -1829,6 +1837,7 @@ pub async fn run_agent( } Err(e) => { let status = resp.status(); + let headers = resp.headers().clone(); let text = resp .text() .await @@ -1898,10 +1907,9 @@ pub async fn run_agent( query_builder = create_chat_completions_query_builder(&credentials); include_usage = true; } else { - return Err(Error::internal_err(format!( - "API error calling {}: {} - {}", - endpoint, e, text - ))); + let message = + format!("API error calling {}: {} - {}", endpoint, e, text); + return Err(response_error(status, &headers, &text, message)); } } } @@ -1961,7 +1969,45 @@ pub async fn run_agent( return Err(error); } } - Err(error) => return Err(error), + Err(error) => { + let Some(transient) = as_transient(&error) else { + return Err(error); + }; + // Readers have no way to take back what they were shown, so a call + // that already streamed part of its answer fails as it is. + if streamed_events() != streamed_before { + append_logs( + &job.id, + &job.workspace_id, + "AI provider call failed after part of its answer had streamed; not retried.\n".to_string(), + conn, + ) + .await; + return Err(into_final_error(error)); + } + let Some(delay) = backoff.next_delay(transient) else { + return Err(into_final_error(error)); + }; + append_logs( + &job.id, + &job.workspace_id, + format!( + "AI provider call failed: {}. Retrying in {:.1}s (retry {}/{MAX_RETRIES}).\n", + transient.message, + delay.as_secs_f64(), + backoff.retries() + ), + conn, + ) + .await; + let mut cancelled = cancel_rx.clone(); + tokio::select! { + _ = tokio::time::sleep(delay) => {} + Ok(_) = cancelled.wait_for(|cancelled| *cancelled) => { + return Err(Error::ExecutionErr("Job cancelled".to_string())); + } + } + } } }; From 70db567a78b1e1bc9e2dcc9ff2139951d778117e Mon Sep 17 00:00:00 2001 From: hugocasa Date: Fri, 9 Oct 2026 20:06:01 +0200 Subject: [PATCH 06/20] fix(cli): count superadmins as able to preserve ownership on push (#11653) Claude-Session: https://claude.ai/code/session_011BzU9zd53JsTShCsbKzYzB Co-authored-by: Claude Opus 5.5 (1M context) --- cli/src/core/permissioned_as.ts | 8 ++++-- cli/test/permissioned_as_context_unit.test.ts | 27 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 cli/test/permissioned_as_context_unit.test.ts diff --git a/cli/src/core/permissioned_as.ts b/cli/src/core/permissioned_as.ts index d0301373da..87ee37a8cb 100644 --- a/cli/src/core/permissioned_as.ts +++ b/cli/src/core/permissioned_as.ts @@ -34,10 +34,14 @@ export async function buildPermissionedAsContext( return undefined; } const user = await wmill.whoami({ workspace }); + // A superadmin acts as admin in every workspace, so the backend preserves + // ownership for them even when their own `usr` row there is not an admin. const userIsAdminOrDeployer = - user.is_admin || (user.groups ?? []).includes("wm_deployers"); + user.is_admin || + user.is_super_admin || + (user.groups ?? []).includes("wm_deployers"); log.debug( - `permissioned_as: user=${user.email}, is_admin=${user.is_admin}, groups=${JSON.stringify(user.groups)}, isAdminOrDeployer=${userIsAdminOrDeployer}` + `permissioned_as: user=${user.email}, is_admin=${user.is_admin}, is_super_admin=${user.is_super_admin}, groups=${JSON.stringify(user.groups)}, isAdminOrDeployer=${userIsAdminOrDeployer}` ); return { userCache: new Map(), diff --git a/cli/test/permissioned_as_context_unit.test.ts b/cli/test/permissioned_as_context_unit.test.ts new file mode 100644 index 0000000000..33a24bf3a1 --- /dev/null +++ b/cli/test/permissioned_as_context_unit.test.ts @@ -0,0 +1,27 @@ +/** + * Regression guard: a superadmin whose `usr` row in the workspace is not an + * admin still preserves ownership on the backend, so the CLI must not refuse + * their push as if it would reassign every item to them. + */ + +import { expect, test } from "bun:test"; +import { mockServices } from "./mock_services.ts"; + +mockServices({ + whoami: async () => ({ + email: "super@windmill.dev", + username: "super", + is_admin: false, + is_super_admin: true, + groups: [], + }), +}); + +const { buildPermissionedAsContext } = await import( + "../src/core/permissioned_as.ts" +); + +test("a superadmin with a non-admin membership counts as able to preserve", async () => { + const ctx = await buildPermissionedAsContext("w", "v1"); + expect(ctx?.userIsAdminOrDeployer).toBe(true); +}); From 8556a7bebe093e0bd61c33d67415fd553f0070ad Mon Sep 17 00:00:00 2001 From: Alexander Petric Date: Fri, 9 Oct 2026 14:20:22 -0400 Subject: [PATCH 07/20] fix: harden app secret validation (#11655) Claude-Session: https://claude.ai/code/session_01Kv72vdCDggCZEjJSmNCwnX Co-authored-by: Claude Opus 5.5 (1M context) --- backend/windmill-api/src/apps.rs | 60 ++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index 818a284e96..54095f0dc6 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -2109,18 +2109,23 @@ async fn get_id_from_secret( prefix: Option<&str>, ) -> Result { let mc = build_crypt(db, w_id).await?; - let decrypted = mc - .decrypt_bytes_to_bytes(&(hex::decode(secret)?)) - .map_err(|e| Error::internal_err(e.to_string()))?; - let mut bytes = str::from_utf8(&decrypted).map_err(to_anyhow)?; - if let Some(prefix) = prefix { - if !bytes.starts_with(prefix) { - return Err(Error::BadRequest("Invalid secret".to_string())); - } - bytes = bytes.strip_prefix(prefix).unwrap_or(""); + id_from_secret(&mc, &secret, prefix) + .ok_or_else(|| Error::BadRequest("Invalid secret".to_string())) +} + +/// The secret comes from the URL of unauthenticated routes, so every way it can be invalid +/// must produce the same response: a caller must never learn why a secret was rejected. +fn id_from_secret( + mc: &magic_crypt::MagicCrypt256, + secret: &str, + prefix: Option<&str>, +) -> Option { + let decrypted = mc.decrypt_bytes_to_bytes(&hex::decode(secret).ok()?).ok()?; + let plaintext = String::from_utf8(decrypted).ok()?; + match prefix { + Some(prefix) => plaintext.strip_prefix(prefix)?.parse().ok(), + None => plaintext.parse().ok(), } - let id: i64 = bytes.parse().map_err(to_anyhow)?; - Ok(id) } async fn get_public_resource( @@ -6834,6 +6839,39 @@ mod policy_tests { } } +#[cfg(test)] +mod app_secret_tests { + use super::{id_from_secret, BUNDLE_SECRET_PREFIX}; + use magic_crypt::MagicCryptTrait; + + #[test] + fn every_invalid_secret_is_rejected_alike() { + let mc = magic_crypt::new_magic_crypt!("workspace-key", 256); + let other = magic_crypt::new_magic_crypt!("another-key", 256); + let valid = hex::encode(mc.encrypt_str_to_bytes("42")); + assert_eq!(id_from_secret(&mc, &valid, None), Some(42)); + let bundle = hex::encode(mc.encrypt_str_to_bytes(format!("{BUNDLE_SECRET_PREFIX}7"))); + assert_eq!( + id_from_secret(&mc, &bundle, Some(BUNDLE_SECRET_PREFIX)), + Some(7) + ); + + let mut tampered = mc.encrypt_str_to_bytes("42"); + *tampered.last_mut().unwrap() ^= 1; + for bad in [ + "not hex".to_string(), + hex::encode(&tampered), + hex::encode(other.encrypt_str_to_bytes("42")), + hex::encode(mc.encrypt_bytes_to_bytes(&[0xff, 0xfe])), + hex::encode(mc.encrypt_str_to_bytes("not a number")), + valid.clone(), + ] { + let prefix = (bad == valid).then_some(BUNDLE_SECRET_PREFIX); + assert_eq!(id_from_secret(&mc, &bad, prefix), None, "{bad}"); + } + } +} + #[cfg(test)] mod raw_app_file_key_tests { use super::validate_raw_app_path_keys; From 0a19d26d5f9623b805a7a4cd2e3fc44255a6efee Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Fri, 9 Oct 2026 21:12:12 +0200 Subject: [PATCH 08/20] chore(main): release 1.830.0 (#11650) * chore(main): release 1.830.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 20 +++ backend/Cargo.lock | 162 +++++++++--------- backend/Cargo.toml | 4 +- .../parsers/windmill-parser-wasm/Cargo.lock | 48 +++--- .../parsers/windmill-parser-wasm/Cargo.toml | 2 +- backend/windmill-api/openapi.yaml | 2 +- benchmarks/lib.ts | 2 +- chat-sdk/package-lock.json | 4 +- chat-sdk/package.json | 2 +- cli/src/core/constants.ts | 2 +- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- lsp/Pipfile | 2 +- openflow.openapi.yaml | 2 +- .../WindmillClient/WindmillClient.psd1 | 2 +- python-client/wmill/pyproject.toml | 2 +- typescript-client/jsr.json | 2 +- typescript-client/package.json | 2 +- version.txt | 2 +- windmill-yaml-validator/package-lock.json | 4 +- windmill-yaml-validator/package.json | 2 +- 22 files changed, 148 insertions(+), 128 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 79d95a4d54..fdce702331 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.829.0" + ".": "1.830.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index f664a8aa97..80939b1b47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [1.830.0](https://github.com/windmill-labs/windmill/compare/v1.829.0...v1.830.0) (2026-10-09) + + +### Features + +* add a jobs:run:dependencies token scope for lock generation ([#11643](https://github.com/windmill-labs/windmill/issues/11643)) ([1dc4ed5](https://github.com/windmill-labs/windmill/commit/1dc4ed5e7fb86c9658f1f877e7b92cd9dab5e283)) +* let operators compose full-code apps when the workspace grants it ([#11229](https://github.com/windmill-labs/windmill/issues/11229)) ([02e4697](https://github.com/windmill-labs/windmill/commit/02e4697157112581e9e2a9303180077126db714b)) +* operators get AI sessions ([#11443](https://github.com/windmill-labs/windmill/issues/11443)) ([338c4e9](https://github.com/windmill-labs/windmill/commit/338c4e921dc3185d886c8b040f253285adf20efb)) +* replace the hub_sync script with an opt-in daily server sync ([#11624](https://github.com/windmill-labs/windmill/issues/11624)) ([2b55a8a](https://github.com/windmill-labs/windmill/commit/2b55a8afbc8a86c6cebe97b1cab02c313b986f22)) +* retry transient AI provider failures before output reaches readers ([#11623](https://github.com/windmill-labs/windmill/issues/11623)) ([d3ddef0](https://github.com/windmill-labs/windmill/commit/d3ddef0b7ec4705aa578c4eca24b96e9152485bd)) +* stop offering deno for new scripts, ship it only on full images ([#11648](https://github.com/windmill-labs/windmill/issues/11648)) ([cda7883](https://github.com/windmill-labs/windmill/commit/cda78830fde1b96bd3de2674e23c245b50e3c051)) + + +### Bug Fixes + +* **cli:** count superadmins as able to preserve ownership on push ([#11653](https://github.com/windmill-labs/windmill/issues/11653)) ([70db567](https://github.com/windmill-labs/windmill/commit/70db567a78b1e1bc9e2dcc9ff2139951d778117e)) +* harden app secret validation ([#11655](https://github.com/windmill-labs/windmill/issues/11655)) ([8556a7b](https://github.com/windmill-labs/windmill/commit/8556a7bebe093e0bd61c33d67415fd553f0070ad)) +* open the chat + menu without waiting on the skills and mcp fetches ([#11645](https://github.com/windmill-labs/windmill/issues/11645)) ([1fea1aa](https://github.com/windmill-labs/windmill/commit/1fea1aa6609d644e02bac8d16186f9a2dfcb63cd)) +* say Running on a chat run card only once its job exists ([#11627](https://github.com/windmill-labs/windmill/issues/11627)) ([d046187](https://github.com/windmill-labs/windmill/commit/d046187e3edd01535240c6080b483afe154ae189)) + ## [1.829.0](https://github.com/windmill-labs/windmill/compare/v1.828.0...v1.829.0) (2026-10-09) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 64a152c962..fe235db7d1 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -14829,7 +14829,7 @@ dependencies = [ [[package]] name = "windmill" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-nats", @@ -14918,7 +14918,7 @@ dependencies = [ [[package]] name = "windmill-ai" -version = "1.829.0" +version = "1.830.0" dependencies = [ "async-stream", "async-trait", @@ -14953,7 +14953,7 @@ dependencies = [ [[package]] name = "windmill-alerting" -version = "1.829.0" +version = "1.830.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14966,7 +14966,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "argon2", @@ -15106,7 +15106,7 @@ dependencies = [ [[package]] name = "windmill-api-agent-workers" -version = "1.829.0" +version = "1.830.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15129,7 +15129,7 @@ dependencies = [ [[package]] name = "windmill-api-assets" -version = "1.829.0" +version = "1.830.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15146,7 +15146,7 @@ dependencies = [ [[package]] name = "windmill-api-auth" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15172,7 +15172,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.829.0" +version = "1.830.0" dependencies = [ "reqwest 0.12.28", "serde", @@ -15182,7 +15182,7 @@ dependencies = [ [[package]] name = "windmill-api-configs" -version = "1.829.0" +version = "1.830.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15199,7 +15199,7 @@ dependencies = [ [[package]] name = "windmill-api-debug" -version = "1.829.0" +version = "1.830.0" dependencies = [ "axum 0.8.9", "base64 0.22.1", @@ -15221,7 +15221,7 @@ dependencies = [ [[package]] name = "windmill-api-embeddings" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15244,7 +15244,7 @@ dependencies = [ [[package]] name = "windmill-api-flow-conversations" -version = "1.829.0" +version = "1.830.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15260,7 +15260,7 @@ dependencies = [ [[package]] name = "windmill-api-flows" -version = "1.829.0" +version = "1.830.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15282,7 +15282,7 @@ dependencies = [ [[package]] name = "windmill-api-groups" -version = "1.829.0" +version = "1.830.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15304,7 +15304,7 @@ dependencies = [ [[package]] name = "windmill-api-inputs" -version = "1.829.0" +version = "1.830.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15318,7 +15318,7 @@ dependencies = [ [[package]] name = "windmill-api-integration-tests" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-nats", @@ -15354,7 +15354,7 @@ dependencies = [ [[package]] name = "windmill-api-jobs" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15380,7 +15380,7 @@ dependencies = [ [[package]] name = "windmill-api-npm-proxy" -version = "1.829.0" +version = "1.830.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15408,7 +15408,7 @@ dependencies = [ [[package]] name = "windmill-api-openapi" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15430,7 +15430,7 @@ dependencies = [ [[package]] name = "windmill-api-schedule" -version = "1.829.0" +version = "1.830.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15450,7 +15450,7 @@ dependencies = [ [[package]] name = "windmill-api-scripts" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15492,7 +15492,7 @@ dependencies = [ [[package]] name = "windmill-api-settings" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15521,7 +15521,7 @@ dependencies = [ [[package]] name = "windmill-api-sse" -version = "1.829.0" +version = "1.830.0" dependencies = [ "lazy_static", "serde", @@ -15533,7 +15533,7 @@ dependencies = [ [[package]] name = "windmill-api-users" -version = "1.829.0" +version = "1.830.0" dependencies = [ "argon2", "axum 0.8.9", @@ -15557,7 +15557,7 @@ dependencies = [ [[package]] name = "windmill-api-workers" -version = "1.829.0" +version = "1.830.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15571,7 +15571,7 @@ dependencies = [ [[package]] name = "windmill-api-workspaces" -version = "1.829.0" +version = "1.830.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15610,7 +15610,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.829.0" +version = "1.830.0" dependencies = [ "chrono", "lazy_static", @@ -15625,7 +15625,7 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15644,7 +15644,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.829.0" +version = "1.830.0" dependencies = [ "aes-gcm", "aho-corasick", @@ -15758,7 +15758,7 @@ dependencies = [ [[package]] name = "windmill-dep-map" -version = "1.829.0" +version = "1.830.0" dependencies = [ "chrono", "futures", @@ -15778,7 +15778,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.829.0" +version = "1.830.0" dependencies = [ "regex", "serde", @@ -15795,7 +15795,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "astral-tokio-tar", @@ -15822,7 +15822,7 @@ dependencies = [ [[package]] name = "windmill-jseval" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "futures", @@ -15839,7 +15839,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.829.0" +version = "1.830.0" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -15855,7 +15855,7 @@ dependencies = [ [[package]] name = "windmill-mcp" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-trait", @@ -15876,7 +15876,7 @@ dependencies = [ [[package]] name = "windmill-native-triggers" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-trait", @@ -15907,7 +15907,7 @@ dependencies = [ [[package]] name = "windmill-oauth" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "arc-swap", @@ -15932,7 +15932,7 @@ dependencies = [ [[package]] name = "windmill-object-store" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-stream", @@ -15968,7 +15968,7 @@ dependencies = [ [[package]] name = "windmill-operator" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "futures", @@ -15986,7 +15986,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.829.0" +version = "1.830.0" dependencies = [ "convert_case 0.6.0", "serde", @@ -15995,7 +15995,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "lazy_static", @@ -16007,7 +16007,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "serde_json", @@ -16019,7 +16019,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "gosyn", @@ -16031,7 +16031,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "lazy_static", @@ -16043,7 +16043,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "serde_json", @@ -16055,7 +16055,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "nu-parser", @@ -16066,7 +16066,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -16077,7 +16077,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -16089,7 +16089,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "rustpython-ast", @@ -16100,7 +16100,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-recursion", @@ -16122,7 +16122,7 @@ dependencies = [ [[package]] name = "windmill-parser-r" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "serde_json", @@ -16134,7 +16134,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "lazy_static", @@ -16148,7 +16148,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -16165,7 +16165,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "lazy_static", @@ -16178,7 +16178,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "serde", @@ -16190,7 +16190,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "lazy_static", @@ -16208,7 +16208,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "serde-wasm-bindgen", @@ -16224,7 +16224,7 @@ dependencies = [ [[package]] name = "windmill-parser-wac" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "rustpython-ast", @@ -16240,7 +16240,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "lazy_static", @@ -16254,7 +16254,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-recursion", @@ -16293,7 +16293,7 @@ dependencies = [ [[package]] name = "windmill-runtime-nativets" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "const_format", @@ -16333,7 +16333,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.829.0" +version = "1.830.0" dependencies = [ "getrandom 0.3.4", "wasm-bindgen", @@ -16344,7 +16344,7 @@ dependencies = [ [[package]] name = "windmill-store" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-recursion", @@ -16379,7 +16379,7 @@ dependencies = [ [[package]] name = "windmill-test-utils" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-trait", @@ -16403,7 +16403,7 @@ dependencies = [ [[package]] name = "windmill-trigger" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-trait", @@ -16436,7 +16436,7 @@ dependencies = [ [[package]] name = "windmill-trigger-amqp" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-trait", @@ -16463,7 +16463,7 @@ dependencies = [ [[package]] name = "windmill-trigger-azure" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-trait", @@ -16496,7 +16496,7 @@ dependencies = [ [[package]] name = "windmill-trigger-email" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-trait", @@ -16516,7 +16516,7 @@ dependencies = [ [[package]] name = "windmill-trigger-gcp" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-trait", @@ -16550,7 +16550,7 @@ dependencies = [ [[package]] name = "windmill-trigger-http" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-trait", @@ -16587,7 +16587,7 @@ dependencies = [ [[package]] name = "windmill-trigger-kafka" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-trait", @@ -16610,7 +16610,7 @@ dependencies = [ [[package]] name = "windmill-trigger-mqtt" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-trait", @@ -16634,7 +16634,7 @@ dependencies = [ [[package]] name = "windmill-trigger-nats" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-nats", @@ -16658,7 +16658,7 @@ dependencies = [ [[package]] name = "windmill-trigger-postgres" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-trait", @@ -16693,7 +16693,7 @@ dependencies = [ [[package]] name = "windmill-trigger-sqs" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-trait", @@ -16721,7 +16721,7 @@ dependencies = [ [[package]] name = "windmill-trigger-websocket" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-trait", @@ -16746,7 +16746,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "bitflags 2.13.2", @@ -16765,7 +16765,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-once-cell", @@ -16884,7 +16884,7 @@ dependencies = [ [[package]] name = "windmill-worker-volumes" -version = "1.829.0" +version = "1.830.0" dependencies = [ "bytes", "futures", @@ -17731,9 +17731,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.1.0+zstd.1.5.7" +version = "2.1.1+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" +checksum = "aeec9eaf2dffbbd09201e23bd0ffcbaa33bb8e9266a10734fd7ed90a85eca078" dependencies = [ "cc", "pkg-config", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 4cdd6adae5..f50a858d52 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.829.0" +version = "1.830.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.829.0" +version = "1.830.0" authors = ["Ruben Fiszel "] edition = "2021" diff --git a/backend/parsers/windmill-parser-wasm/Cargo.lock b/backend/parsers/windmill-parser-wasm/Cargo.lock index 2180b0656d..c0108282ee 100644 --- a/backend/parsers/windmill-parser-wasm/Cargo.lock +++ b/backend/parsers/windmill-parser-wasm/Cargo.lock @@ -6216,7 +6216,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windmill-common" -version = "1.829.0" +version = "1.830.0" dependencies = [ "aho-corasick", "anyhow", @@ -6302,7 +6302,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.829.0" +version = "1.830.0" dependencies = [ "proc-macro2", "quote", @@ -6314,7 +6314,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.829.0" +version = "1.830.0" dependencies = [ "convert_case", "serde", @@ -6323,7 +6323,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "lazy_static", @@ -6335,7 +6335,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "serde_json", @@ -6347,7 +6347,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "gosyn", @@ -6359,7 +6359,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "lazy_static", @@ -6371,7 +6371,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "serde_json", @@ -6383,7 +6383,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "nu-parser", @@ -6394,7 +6394,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -6405,7 +6405,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "rustpython-ast", @@ -6428,7 +6428,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "async-recursion", @@ -6450,7 +6450,7 @@ dependencies = [ [[package]] name = "windmill-parser-r" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "serde_json", @@ -6462,7 +6462,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "lazy_static", @@ -6476,7 +6476,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "convert_case", @@ -6493,7 +6493,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "lazy_static", @@ -6506,7 +6506,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "serde", @@ -6518,7 +6518,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "lazy_static", @@ -6536,7 +6536,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "serde-wasm-bindgen", @@ -6552,7 +6552,7 @@ dependencies = [ [[package]] name = "windmill-parser-wac" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "rustpython-ast", @@ -6568,7 +6568,7 @@ dependencies = [ [[package]] name = "windmill-parser-wasm" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "getrandom 0.2.17", @@ -6600,7 +6600,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "lazy_static", @@ -6614,7 +6614,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.829.0" +version = "1.830.0" dependencies = [ "anyhow", "bitflags", diff --git a/backend/parsers/windmill-parser-wasm/Cargo.toml b/backend/parsers/windmill-parser-wasm/Cargo.toml index 2592d0bad4..8aebf20597 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.829.0" +version = "1.830.0" edition = "2021" authors = ["Ruben Fiszel "] diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 6e3c6ae615..43b1f776c2 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.829.0 + version: 1.830.0 title: Windmill API contact: diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index 6fbac6f559..34e4da5cf3 100644 --- a/benchmarks/lib.ts +++ b/benchmarks/lib.ts @@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts"; import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts"; import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts"; -export const VERSION = "v1.829.0"; +export const VERSION = "v1.830.0"; export async function login(email: string, password: string): Promise { return await windmill.UserService.login({ diff --git a/chat-sdk/package-lock.json b/chat-sdk/package-lock.json index 308927fefe..03a55cff86 100644 --- a/chat-sdk/package-lock.json +++ b/chat-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-chat", - "version": "1.829.0", + "version": "1.830.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-chat", - "version": "1.829.0", + "version": "1.830.0", "license": "Apache-2.0", "devDependencies": { "@ai-sdk/react": "^4.0.102", diff --git a/chat-sdk/package.json b/chat-sdk/package.json index c1e57bcc1c..96637c376c 100644 --- a/chat-sdk/package.json +++ b/chat-sdk/package.json @@ -1,7 +1,7 @@ { "name": "windmill-chat", "description": "Build chat interfaces on Windmill flows deployed in chat mode, from any frontend or raw app", - "version": "1.829.0", + "version": "1.830.0", "author": "Ruben Fiszel", "license": "Apache-2.0", "homepage": "https://github.com/windmill-labs/windmill/tree/main/chat-sdk#readme", diff --git a/cli/src/core/constants.ts b/cli/src/core/constants.ts index 837dc4cf57..21aeb07f60 100644 --- a/cli/src/core/constants.ts +++ b/cli/src/core/constants.ts @@ -10,4 +10,4 @@ export const WM_FORK_PREFIX = "wm-fork"; // (e.g. utils.ts) can read it without importing main.ts and creating a circular // dependency (main → workspace → utils → main) that triggers a TDZ. // Re-exported from main.ts for backwards compatibility. -export const VERSION = "1.829.0"; +export const VERSION = "1.830.0"; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 774e9db0df..c8250f9eca 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "@windmill-labs/components", - "version": "1.829.0", + "version": "1.830.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@windmill-labs/components", - "version": "1.829.0", + "version": "1.830.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/frontend/package.json b/frontend/package.json index 25924993cf..ddfc6b410b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "@windmill-labs/components", - "version": "1.829.0", + "version": "1.830.0", "scripts": { "dev": "vite dev", "dev:ui-builder": "mv static/ui_builder static/ui_builder.dev-disabled 2>/dev/null || true ; trap 'mv static/ui_builder.dev-disabled static/ui_builder 2>/dev/null || true' EXIT ; vite dev", diff --git a/lsp/Pipfile b/lsp/Pipfile index 84ddb02401..4aee9bdf9f 100644 --- a/lsp/Pipfile +++ b/lsp/Pipfile @@ -4,7 +4,7 @@ verify_ssl = true name = "pypi" [packages] -wmill = ">=1.829.0" +wmill = ">=1.830.0" sendgrid = "*" mysql-connector-python = "*" pymongo = "*" diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index 0e186c74bd..e3759ee6a6 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -1,7 +1,7 @@ openapi: '3.0.3' info: - version: 1.829.0 + version: 1.830.0 title: OpenFlow Spec contact: name: Ruben Fiszel diff --git a/powershell-client/WindmillClient/WindmillClient.psd1 b/powershell-client/WindmillClient/WindmillClient.psd1 index fb5186c37a..8385126639 100644 --- a/powershell-client/WindmillClient/WindmillClient.psd1 +++ b/powershell-client/WindmillClient/WindmillClient.psd1 @@ -12,7 +12,7 @@ RootModule = 'WindmillClient.psm1' # Version number of this module. - ModuleVersion = '1.829.0' + ModuleVersion = '1.830.0' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/python-client/wmill/pyproject.toml b/python-client/wmill/pyproject.toml index 104455b7f9..411bc09cc8 100644 --- a/python-client/wmill/pyproject.toml +++ b/python-client/wmill/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill" -version = "1.829.0" +version = "1.830.0" description = "A client library for accessing Windmill server wrapping the Windmill client API" license = "Apache-2.0" homepage = "https://windmill.dev" diff --git a/typescript-client/jsr.json b/typescript-client/jsr.json index ff319ebeea..ffd7c2300a 100644 --- a/typescript-client/jsr.json +++ b/typescript-client/jsr.json @@ -1,6 +1,6 @@ { "name": "@windmill/windmill", - "version": "1.829.0", + "version": "1.830.0", "exports": "./src/index.ts", "publish": { "exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts", "./wacError.ts"] diff --git a/typescript-client/package.json b/typescript-client/package.json index b62ad56266..9adbfcec62 100644 --- a/typescript-client/package.json +++ b/typescript-client/package.json @@ -1,7 +1,7 @@ { "name": "windmill-client", "description": "Windmill SDK client for browsers and Node.js", - "version": "1.829.0", + "version": "1.830.0", "author": "Ruben Fiszel", "license": "Apache 2.0", "homepage": "https://github.com/windmill-labs/windmill/tree/main/typescript-client#readme", diff --git a/version.txt b/version.txt index bb120b1ee4..54f09db472 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.829.0 +1.830.0 diff --git a/windmill-yaml-validator/package-lock.json b/windmill-yaml-validator/package-lock.json index 6b3afa01f6..95c5128a13 100644 --- a/windmill-yaml-validator/package-lock.json +++ b/windmill-yaml-validator/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-yaml-validator", - "version": "1.829.0", + "version": "1.830.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-yaml-validator", - "version": "1.829.0", + "version": "1.830.0", "license": "Apache 2.0", "dependencies": { "@stoplight/yaml": "^4.3.0", diff --git a/windmill-yaml-validator/package.json b/windmill-yaml-validator/package.json index 7e00259bbb..6f2c2d7fed 100644 --- a/windmill-yaml-validator/package.json +++ b/windmill-yaml-validator/package.json @@ -1,6 +1,6 @@ { "name": "windmill-yaml-validator", - "version": "1.829.0", + "version": "1.830.0", "description": "YAML validator for Windmill flow, schedule, and trigger files", "main": "dist/index.js", "types": "dist/index.d.ts", From 93e81ec1899778dc4d5e79d10cb08992b0da8c18 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Fri, 9 Oct 2026 21:54:57 +0200 Subject: [PATCH 09/20] [ee] chore: upgrade native runtime to current deno crates and v8 150 (#11657) * chore: upgrade the native runtime to the current deno crates and v8 150 Co-Authored-By: Claude Opus 5.5 * fix: keep data and blob fetches working under no_network Co-Authored-By: Claude Opus 5.5 * fix: pin the simdutf v8 archive and keep telemetry on http exporters Co-Authored-By: Claude Opus 5.5 * fix: force the http otlp exporter for native telemetry and pin v8 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- backend/Cargo.lock | 1650 ++++++++++++----- backend/Cargo.toml | 52 +- backend/ee-repo-ref.txt | 2 +- backend/windmill-runtime-nativets/Cargo.toml | 13 +- backend/windmill-runtime-nativets/build.rs | 222 +-- backend/windmill-runtime-nativets/src/lib.rs | 269 ++- .../windmill-runtime-nativets/src/runtime.js | 121 +- .../src/smoke_tests.rs | 4 + .../tests/no_network.rs | 12 + .../tests/otel_e2e.rs | 15 +- .../tests/otel_init.rs | 11 +- flake.nix | 9 +- 12 files changed, 1485 insertions(+), 895 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index fe235db7d1..efd46867fb 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -508,6 +508,22 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "asn1-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +dependencies = [ + "asn1-rs-derive 0.4.0", + "asn1-rs-impl 0.1.0", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "asn1-rs" version = "0.6.2" @@ -515,7 +531,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" dependencies = [ "asn1-rs-derive 0.5.1", - "asn1-rs-impl", + "asn1-rs-impl 0.2.0", "displaydoc", "nom", "num-traits", @@ -531,7 +547,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" dependencies = [ "asn1-rs-derive 0.6.0", - "asn1-rs-impl", + "asn1-rs-impl 0.2.0", "displaydoc", "nom", "num-traits", @@ -540,6 +556,18 @@ dependencies = [ "time", ] +[[package]] +name = "asn1-rs-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure 0.12.6", +] + [[package]] name = "asn1-rs-derive" version = "0.5.1" @@ -564,6 +592,17 @@ dependencies = [ "synstructure 0.13.2", ] +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "asn1-rs-impl" version = "0.2.0" @@ -974,11 +1013,23 @@ version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" dependencies = [ - "aws-lc-sys", + "aws-lc-sys 0.45.0", "untrusted 0.7.1", "zeroize", ] +[[package]] +name = "aws-lc-sys" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "aws-lc-sys" version = "0.45.0" @@ -1349,7 +1400,7 @@ dependencies = [ "hyper-util", "pin-project-lite", "rustls 0.21.12", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-native-certs 0.8.4", "rustls-pki-types", "tokio", @@ -1492,44 +1543,17 @@ dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", "aws-smithy-types", - "rustc_version 0.4.1", + "rustc_version", "tracing", ] -[[package]] -name = "axum" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" -dependencies = [ - "async-trait", - "axum-core 0.4.5", - "bytes", - "futures-util", - "http 1.5.0", - "http-body 1.1.0", - "http-body-util", - "itoa", - "matchit 0.7.3", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "sync_wrapper", - "tower 0.5.3", - "tower-layer", - "tower-service", -] - [[package]] name = "axum" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ - "axum-core 0.5.6", + "axum-core", "axum-macros", "bytes", "form_urlencoded", @@ -1558,26 +1582,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 1.5.0", - "http-body 1.1.0", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", - "tower-layer", - "tower-service", -] - [[package]] name = "axum-core" version = "0.5.6" @@ -1608,12 +1612,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "az" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be5eb007b7cacc6c660343e96f650fedf4b5a77512399eb952ca6642cf8d13f7" - [[package]] name = "backon" version = "1.6.0" @@ -1646,6 +1644,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + [[package]] name = "base32" version = "0.5.1" @@ -1723,6 +1727,26 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.13.2", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease 0.2.37", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex 1.3.0", + "syn 2.0.119", +] + [[package]] name = "bindgen" version = "0.71.1" @@ -2018,6 +2042,17 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "brotli" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor 4.0.3", +] + [[package]] name = "brotli" version = "7.0.0" @@ -2212,6 +2247,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "calendrical_calculations" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5abbd6eeda6885048d357edc66748eea6e0268e3dd11f326fff5bd248d779c26" +dependencies = [ + "core_maths", + "displaydoc", +] + [[package]] name = "candle-core" version = "0.9.2" @@ -2490,8 +2535,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b77c319abfd5219629c45c34c89ba945ed3c5e49fcde9d16b6c3885f118a730" dependencies = [ "const-oid 0.9.6", - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", "x509-cert", ] @@ -2562,6 +2607,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "const-hex" +version = "1.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e59eef12462b0f9b0a3620219be5d639afd79fe39dff0a42c3997061f9298b4" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "proptest", + "serde_core", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -2700,6 +2757,21 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -2751,6 +2823,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "cron" version = "0.17.0" @@ -2842,6 +2920,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "crypto-bigint" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +dependencies = [ + "cpubits", + "ctutils", + "getrandom 0.4.3", + "hybrid-array", + "num-traits", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -2859,7 +2953,32 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ + "getrandom 0.4.3", "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.119", ] [[package]] @@ -2899,6 +3018,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574" dependencies = [ "cmov", + "subtle", ] [[package]] @@ -2926,8 +3046,8 @@ dependencies = [ "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest 0.10.7", - "fiat-crypto 0.2.9", - "rustc_version 0.4.1", + "fiat-crypto", + "rustc_version", "subtle", "zeroize", ] @@ -3801,7 +3921,7 @@ checksum = "c72e0409b3dbd60a5bf296cbc273a8e36bb3a3aab6abac389f1891e187d5ce14" dependencies = [ "base64 0.22.1", "capacity_builder", - "deno_error 0.7.3", + "deno_error", "deno_media_type", "deno_terminal", "dprint-swc-ext", @@ -3835,37 +3955,29 @@ dependencies = [ "url", ] -[[package]] -name = "deno_console" -version = "0.209.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66c0b8a65dcb7b38c22e5969c6454b3cb0839b7dcfb7a4f0d904de871a4c4416" -dependencies = [ - "deno_core", -] - [[package]] name = "deno_core" -version = "0.352.1" +version = "0.412.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf78f3f72ac8e09b18a588bc26a1b3c5ab853e7fb489889b2f5000654d34db5d" +checksum = "580cb2e0d797d75ff3dfc4a8f2b14aa7470f0fa31654a09a83d4cbb42b5eb9b4" dependencies = [ "anyhow", - "az", - "bincode", + "base64 0.22.1", "bit-set", "bit-vec 0.8.0", + "boxed_error", "bytes", - "capacity_builder", "cooked-waker", - "deno_core_icudata", - "deno_error 0.6.1", + "deno_error", "deno_ops", "deno_path_util", "deno_unsync", + "deno_v8", "futures", "indexmap 2.14.2", + "inventory", "libc", + "num-bigint", "parking_lot", "percent-encoding", "pin-project", @@ -3875,28 +3987,25 @@ dependencies = [ "smallvec", "sourcemap", "static_assertions", + "sys_traits", "thiserror 2.0.21", "tokio", "url", - "v8", + "uuid", "wasm_dep_analyzer", + "windows-sys 0.59.0", ] -[[package]] -name = "deno_core_icudata" -version = "0.74.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4dccb6147bb3f3ba0c7a48e993bfeb999d2c2e47a81badee80e2b370c8d695" - [[package]] name = "deno_crypto" -version = "0.223.0" +version = "0.273.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beb2568806b930b96fb6440ff16463befa73a36de3378986644e7134c9d52e8f" +checksum = "d9931d22328961b9a9c8d3b666a27312c1f3ddcc7ea78d8610ddfbc934ac61a0" dependencies = [ "aes 0.8.3", "aes-gcm", "aes-kw", + "argon2", "aws-lc-rs", "base64 0.22.1", "cbc", @@ -3904,12 +4013,15 @@ dependencies = [ "ctr", "curve25519-dalek", "deno_core", - "deno_error 0.6.1", - "deno_web", + "deno_crypto_provider", + "deno_error", "ecdsa", - "ed448-goldilocks", - "elliptic-curve", + "elliptic-curve 0.13.8", + "fips203", + "fips205", + "hmac 0.12.1", "num-traits", + "ocb3", "once_cell", "p256", "p384", @@ -3920,21 +4032,34 @@ dependencies = [ "serde_bytes", "sha1", "sha2 0.10.9", - "signature", - "spki", + "sha3", + "signature 2.2.0", + "spki 0.7.3", + "subtle", "thiserror 2.0.21", + "tiny-keccak", "tokio", "uuid", "x25519-dalek", ] [[package]] -name = "deno_error" -version = "0.6.1" +name = "deno_crypto_provider" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612ec3fc481fea759141b0c57810889b0a4fb6fee8f10748677bfe492fd30486" +checksum = "fafd673e5a4eefd6d65db358b00ff60488cfa1be6515406b4371935c2e2e6baa" dependencies = [ - "deno_error_macro 0.6.1", + "aws-lc-sys 0.40.0", + "ed448-goldilocks", +] + +[[package]] +name = "deno_error" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfafd2219b29886a71aecbb3449e462deed1b2c474dc5b12f855f0e58c478931" +dependencies = [ + "deno_error_macro", "libc", "serde", "serde_json", @@ -3942,32 +4067,11 @@ dependencies = [ "url", ] -[[package]] -name = "deno_error" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3007d3f1ea92ea503324ae15883aac0c2de2b8cf6fead62203ff6a67161007ab" -dependencies = [ - "deno_error_macro 0.7.3", - "libc", -] - [[package]] name = "deno_error_macro" -version = "0.6.1" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8380a4224d5d2c3f84da4d764c4326cac62e9a1e3d4960442d29136fc07be863" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "deno_error_macro" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b565e60a9685cdf312c888665b5f8647ac692a7da7e058a5e2268a466da8eaf" +checksum = "1c28ede88783f14cd8aae46ca89f230c226b40e4a81ab06fa52ed72af84beb2f" dependencies = [ "proc-macro2", "quote", @@ -3976,31 +4080,30 @@ dependencies = [ [[package]] name = "deno_features" -version = "0.6.0" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487773bb24b92f3b88d1c9ef0d4d15888641a2e2ea4d4cdfd6fda12cae26317c" +checksum = "2fef16a40daeb19acfc5345aa5bb367a36ecffef18b7ad1a594f0f01fe7c20c5" dependencies = [ "deno_core", - "serde", - "serde_json", ] [[package]] name = "deno_fetch" -version = "0.233.0" +version = "0.283.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbe158bec955790105ded69978b1245b97c65092f7bf184add344c0e78efffa" +checksum = "20e792f76d45e20cf6f1ad46157bbe1a0a895a6f09ec4d99b0911112be8e05e3" dependencies = [ + "async-compression", "base64 0.22.1", "bytes", "data-url", "deno_core", - "deno_error 0.6.1", + "deno_error", "deno_fs", + "deno_io", "deno_path_util", "deno_permissions", "deno_tls", - "dyn-clone", "error_reporter", "h2 0.4.20", "hickory-resolver", @@ -4011,8 +4114,7 @@ dependencies = [ "hyper-util", "ipnet", "percent-encoding", - "rustls-webpki 0.102.8", - "serde", + "rustls-webpki 0.103.15", "serde_json", "thiserror 2.0.21", "tokio", @@ -4021,62 +4123,69 @@ dependencies = [ "tokio-util", "tokio-vsock", "tower 0.5.3", - "tower-http", "tower-service", ] [[package]] name = "deno_fs" -version = "0.119.0" +version = "0.169.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca83a95cea7bcdf19dac1888ef6f04aff69d251640a4d085bc6089c06b4b181" +checksum = "a0b0ae3c55f4af9d33113ba4935828a2e9a7bfcbf4d3058533403b9ef332a966" dependencies = [ "async-trait", - "base32", "boxed_error", "deno_core", - "deno_error 0.6.1", + "deno_error", "deno_io", + "deno_maybe_sync", "deno_path_util", "deno_permissions", "filetime", "junction", "libc", - "nix 0.27.1", + "nix 0.30.1", "rand 0.8.5", "rayon", + "same-file", "serde", "thiserror 2.0.21", - "winapi", + "tokio", "windows-sys 0.59.0", ] [[package]] name = "deno_io" -version = "0.119.0" +version = "0.169.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597aac25be261a1bd545d6f47a80439c60e600c076f9847b3718a592883b2a73" +checksum = "ba495b6c141d34e49fb2dbe7ca746d7ac068b1962470776eb341c59059b758f6" dependencies = [ "async-trait", "deno_core", - "deno_error 0.6.1", + "deno_error", + "deno_permissions", "deno_subprocess_windows", "filetime", - "fs3", "libc", "log", - "nix 0.27.1", - "once_cell", + "nix 0.30.1", "os_pipe", "parking_lot", "pin-project", "rand 0.8.5", "tokio", "uuid", - "winapi", "windows-sys 0.59.0", ] +[[package]] +name = "deno_maybe_sync" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e5e72b38015b5e478b8d739dfa818d563329e03a72d6ce5cfb2ecbcd7f8d41" +dependencies = [ + "rustc-hash 2.1.3", +] + [[package]] name = "deno_media_type" version = "0.3.3" @@ -4103,17 +4212,21 @@ dependencies = [ [[package]] name = "deno_net" -version = "0.201.0" +version = "0.251.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb88f1ea2762065d6cc0316372acc22696ab6004943aa029f80546f9faf1b491" +checksum = "19ba02152d978f30e2eb5f8d074b343363f53cb1364b5c304b3176da916a06c0" dependencies = [ "deno_core", - "deno_error 0.6.1", + "deno_error", "deno_features", "deno_permissions", + "deno_signals", "deno_tls", + "deno_tunnel", "hickory-proto", "hickory-resolver", + "libc", + "log", "pin-project", "quinn", "rustls-tokio-stream", @@ -4125,32 +4238,33 @@ dependencies = [ "tokio-vsock", "url", "web-transport-proto", + "x509-parser 0.15.1", ] [[package]] name = "deno_ops" -version = "0.228.1" +version = "0.288.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf8dbe5abf37d270bb853c5dfe45fbe3b1b6c453877cc11d7fe84e9862a6dbc" +checksum = "977748624aad80164dbe385c8c907bc0041c2e6849f92528e362e03132c64c52" dependencies = [ "indexmap 2.14.2", - "proc-macro-rules", "proc-macro2", "quote", "stringcase", "strum", "strum_macros", "syn 2.0.119", + "syn-match", "thiserror 2.0.21", ] [[package]] name = "deno_path_util" -version = "0.4.0" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516f813389095889776b81cc9108ff6f336fd9409b4b12fc0138aea23d2708e1" +checksum = "78c7e98943f0d068928906db0c7bde89de684fa32c6a8018caacc4cee2cdd72b" dependencies = [ - "deno_error 0.6.1", + "deno_error", "percent-encoding", "sys_traits", "thiserror 2.0.21", @@ -4159,77 +4273,100 @@ dependencies = [ [[package]] name = "deno_permissions" -version = "0.68.0" +version = "0.118.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baa14d9c3fbba59836ffbaac7f736a2f48d4a5fc209a2103d1e1b898915232a5" +checksum = "f8e9d37215f52e29b5ad9d9065c476168ffb888cd2ad93b0ae153f22304851a0" dependencies = [ "capacity_builder", - "deno_error 0.6.1", + "chrono", + "deno_error", "deno_path_util", "deno_terminal", "deno_unsync", + "dunce", "fqdn", - "ipnetwork", + "ipnet", "libc", "log", - "nix 0.27.1", + "nix 0.30.1", "once_cell", "parking_lot", - "percent-encoding", "serde", "serde_json", "sys_traits", - "temp_deno_which", "thiserror 2.0.21", + "unicode-normalization", "url", - "winapi", + "which 8.0.6", + "windows-sys 0.59.0", +] + +[[package]] +name = "deno_signals" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038ed4b09a6d36c81ac436c4d10076e9e01cc67faf93d701c541887f8eeddf36" +dependencies = [ + "libc", + "signal-hook", + "thiserror 2.0.21", + "tokio", "windows-sys 0.59.0", ] [[package]] name = "deno_subprocess_windows" -version = "0.4.0" +version = "0.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bc6b10059f0ccb14c6e0319c5275f0407fb2f9ffe405cd555700561999ea4bf" +checksum = "04d4f451b2f94d2847de4895d39ca5aa264b90af34e72fd79ddc1d0da05a5ea0" dependencies = [ - "fastrand 2.5.0", "futures-channel", "libc", + "rand 0.8.5", "windows-sys 0.59.0", ] [[package]] name = "deno_telemetry" -version = "0.31.0" +version = "0.81.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "377581966bd34e85ce230f7f4b1dee7e16c6bf7f845b4690648507ce7ba105d9" +checksum = "8831dc236ed1c66a90c8dfddab93ad44fbb28950fc476c78306b9100233ff694" dependencies = [ "async-trait", "deno_core", - "deno_error 0.6.1", + "deno_error", + "deno_net", + "deno_signals", + "deno_terminal", "deno_tls", + "flate2", "http-body-util", "hyper 1.12.0", "hyper-rustls 0.27.10", "hyper-util", "log", "once_cell", - "opentelemetry 0.27.1", - "opentelemetry-http 0.27.0", - "opentelemetry-otlp 0.27.0", - "opentelemetry-semantic-conventions 0.27.0", - "opentelemetry_sdk 0.27.1", + "opentelemetry 0.32.0", + "opentelemetry-http 0.32.0", + "opentelemetry-otlp 0.32.0", + "opentelemetry-proto 0.32.0", + "opentelemetry-semantic-conventions 0.32.1", + "opentelemetry_sdk 0.32.1", "pin-project", + "prost 0.14.4", "serde", + "sys_traits", "thiserror 2.0.21", "tokio", + "tokio-vsock", + "tower-service", ] [[package]] name = "deno_terminal" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23f71c27009e0141dedd315f1dfa3ebb0a6ca4acce7c080fac576ea415a465f6" +checksum = "f3ba8041ae7319b3ca6a64c399df4112badcbbe0868b4517637647614bede4be" dependencies = [ "once_cell", "termcolor", @@ -4237,23 +4374,40 @@ dependencies = [ [[package]] name = "deno_tls" -version = "0.196.0" +version = "0.246.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7835eb6a8d114703b0293573fbb8885bc2c81e8c35363b9e89391dd3c541fa6" +checksum = "6229f7a00441b2acbf7fae58dd14bded0e835f212653402083430826e640f99d" dependencies = [ "deno_core", - "deno_error 0.6.1", + "deno_error", "deno_native_certs", - "rustls 0.23.35", + "log", + "rustls 0.23.40", "rustls-pemfile 2.2.0", "rustls-tokio-stream", - "rustls-webpki 0.102.8", + "rustls-webpki 0.103.15", "serde", "thiserror 2.0.21", "tokio", "webpki-roots 0.26.11", ] +[[package]] +name = "deno_tunnel" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e432929e0c167f2a003b963ee406c714fcd947a87d4f9edc7b3255e87be9881d" +dependencies = [ + "pin-project", + "quinn", + "serde", + "serde_json", + "socket2 0.6.5", + "thiserror 2.0.21", + "tokio", + "tracing", +] + [[package]] name = "deno_unsync" version = "0.4.4" @@ -4266,42 +4420,43 @@ dependencies = [ ] [[package]] -name = "deno_url" -version = "0.209.0" +name = "deno_v8" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cccbb10fb59f29b04161055a65f7fe83364f9e8317d4d2ce3bd7979faf63a87" +checksum = "85cdff08dedf79da6a40f97554f47d59d419ba3df8061b8b31789cd2cbe00433" dependencies = [ - "deno_core", - "deno_error 0.6.1", - "urlpattern", + "v8", + "v8x", ] [[package]] name = "deno_web" -version = "0.240.0" +version = "0.290.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6b8a97cc90b6aaea20fe200cfa7e5b6953dd33ecda0af50d6d360387c33df4" +checksum = "7510967ba4ec695959c1a518748c3b564fd9ca7a8f3eb13bd397707a3ca232da" dependencies = [ "async-trait", - "base64-simd", + "brotli 6.0.0", "bytes", + "cssparser", "deno_core", - "deno_error 0.6.1", - "deno_permissions", + "deno_error", "encoding_rs", "flate2", "futures", "serde", "thiserror 2.0.21", "tokio", + "unicode-normalization", + "urlpattern", "uuid", ] [[package]] name = "deno_webidl" -version = "0.209.0" +version = "0.259.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a20bbfac0cf15918f7cbd0b55c366ac20ad65c825f362fa684c3104984254d3b" +checksum = "40af42313723a8d3114c4e5b007abd1cab4400a0b68929794c556b2e792409c7" dependencies = [ "deno_core", ] @@ -4319,6 +4474,30 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" +dependencies = [ + "const-oid 0.10.2", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +dependencies = [ + "asn1-rs 0.5.2", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "der-parser" version = "9.0.0" @@ -4367,6 +4546,25 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive-io" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58cc7f4740088458993d183a200fe56e62378736f94bf0e2dd45807407e7bb94" +dependencies = [ + "derive-io-macros", + "tokio", +] + +[[package]] +name = "derive-io-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d0a1bd7eeab72097740967d03d53db5fbaf8e3c0dd471ebdefa43ce445a20a6" +dependencies = [ + "proc-macro2", +] + [[package]] name = "derive_builder" version = "0.12.0" @@ -4467,7 +4665,7 @@ checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "proc-macro2", "quote", - "rustc_version 0.4.1", + "rustc_version", "syn 2.0.119", ] @@ -4524,6 +4722,38 @@ dependencies = [ "ctutils", ] +[[package]] +name = "diplomat" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b37a6cea72eb6df5143d3e091d4c34cb0cf4046dd1ddcc082988886fe1f809" +dependencies = [ + "diplomat_core", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "diplomat-runtime" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae517853d9f82b2d144393b1c32c34c46c2e361de2b6c44a8fdfce23beb211cd" + +[[package]] +name = "diplomat_core" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b18e806f9ecb92df828d080efe47984ae1193958295d4a84fb8b32c9a1aa1b" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "smallvec", + "strck", + "syn 3.0.6", +] + [[package]] name = "dirs" version = "4.0.0" @@ -4681,6 +4911,21 @@ dependencies = [ "text_lines", ] +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + [[package]] name = "duct" version = "0.13.7" @@ -4727,12 +4972,12 @@ version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der", + "der 0.7.10", "digest 0.10.7", - "elliptic-curve", + "elliptic-curve 0.13.8", "rfc6979", - "signature", - "spki", + "signature 2.2.0", + "spki 0.7.3", ] [[package]] @@ -4741,8 +4986,8 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", - "signature", + "pkcs8 0.10.2", + "signature 2.2.0", ] [[package]] @@ -4756,21 +5001,35 @@ dependencies = [ "rand_core 0.6.4", "serde", "sha2 0.10.9", - "signature", + "signature 2.2.0", "subtle", "zeroize", ] [[package]] -name = "ed448-goldilocks" -version = "0.8.3" +name = "ed448" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06924531e9e90130842b012e447f85bdaf9161bc8a0f8092be8cb70b01ebe092" +checksum = "ae112a25f86ae3598d4e8533ed1e65149cec6eb21918e7a6f4c06dddec370263" dependencies = [ - "fiat-crypto 0.1.20", - "hex", + "pkcs8 0.11.0", + "signature 3.0.0", +] + +[[package]] +name = "ed448-goldilocks" +version = "0.14.0-pre.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b805154de2e68f59874ec217ca36790dcffe500cd872c60fe509d28d0814a74d" +dependencies = [ + "ed448", + "elliptic-curve 0.14.1", + "hash2curve", + "rand_core 0.10.1", + "serdect 0.4.3", + "shake", + "signature 3.0.0", "subtle", - "zeroize", ] [[package]] @@ -4800,20 +5059,39 @@ version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct", + "base16ct 0.2.0", "base64ct", - "crypto-bigint", + "crypto-bigint 0.5.5", "digest 0.10.7", - "ff", + "ff 0.13.1", "generic-array", - "group", + "group 0.13.0", "hkdf", "pem-rfc7468", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", - "sec1", + "sec1 0.7.3", "serde_json", - "serdect", + "serdect 0.2.0", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" +dependencies = [ + "base16ct 1.0.0", + "crypto-bigint 0.7.5", + "crypto-common 0.2.2", + "ff 0.14.0", + "group 0.14.0", + "hybrid-array", + "pkcs8 0.11.0", + "rand_core 0.10.1", + "sec1 0.8.1", "subtle", "zeroize", ] @@ -5012,6 +5290,18 @@ dependencies = [ "regex-syntax 0.8.11", ] +[[package]] +name = "fastbloom" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef975e30683b2d965054bb0a836f8973857c4ebf6acf274fe46617cd285060d8" +dependencies = [ + "foldhash 0.2.0", + "libm", + "portable-atomic", + "siphasher 1.0.4", +] + [[package]] name = "fastdivide" version = "0.4.2" @@ -5044,10 +5334,14 @@ dependencies = [ ] [[package]] -name = "fiat-crypto" -version = "0.1.20" +name = "ff" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core 0.10.1", + "subtle", +] [[package]] name = "fiat-crypto" @@ -5071,6 +5365,30 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" +[[package]] +name = "fips203" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8bdb6454f692ca2a2b45cd554c6828c639d7f9c968cf83a678899ec4443a280" +dependencies = [ + "rand_core 0.6.4", + "sha3", + "subtle", + "zeroize", +] + +[[package]] +name = "fips205" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f5626bf5534df4ebdbd2536465d7eaa8a9dc2cdeb7e036e0ecf291dcc80ffb6" +dependencies = [ + "rand_core 0.6.4", + "sha2 0.10.9", + "sha3", + "zeroize", +] + [[package]] name = "fixedbitset" version = "0.5.7" @@ -5090,7 +5408,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3" dependencies = [ "bitflags 2.13.2", - "rustc_version 0.4.1", + "rustc_version", ] [[package]] @@ -5173,9 +5491,9 @@ dependencies = [ [[package]] name = "fqdn" -version = "0.3.12" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb540cf7bc4fe6df9d8f7f0c974cfd0dce8ed4e9e8884e73433b503ee78b4e7d" +checksum = "886ac788f62d16d6b0f26b2fa762b34ef16ebfb4b624c2c15fbcadc9173c0f72" [[package]] name = "from_variant" @@ -5193,17 +5511,6 @@ version = "1.20260821.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28c70238056ad1dbfdf035222a571d2e9baf5b6a4c308cfa87bc8cbe7f2f3840" -[[package]] -name = "fs3" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb17cf6ed704f72485332f6ab65257460c4f9f3083934cf402bf9f5b3b600a90" -dependencies = [ - "libc", - "rustc_version 0.2.3", - "winapi", -] - [[package]] name = "fs4" version = "0.13.1" @@ -5721,7 +6028,7 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "886aa8ec755382a1fdf4651f6e6ec01f2f3bf49f2cb0f068b9a74cafd574a715" dependencies = [ - "prost", + "prost 0.13.5", "prost-types", "tonic 0.12.3", ] @@ -5783,11 +6090,22 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff", + "ff 0.13.1", "rand_core 0.6.4", "subtle", ] +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff 0.14.0", + "rand_core 0.10.1", + "subtle", +] + [[package]] name = "gzip-header" version = "1.1.0" @@ -5850,6 +6168,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hash2curve" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1eaf40612d7d854743e7189228a6d528f0f6e8502cf6a0cb831d28a218b7f3f6" +dependencies = [ + "digest 0.11.3", + "elliptic-curve 0.14.1", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -5997,11 +6325,10 @@ dependencies = [ [[package]] name = "hickory-proto" -version = "0.25.0-alpha.5" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d00147af6310f4392a31680db52a3ed45a2e0f68eb18e8c3fe5537ecc96d9e2" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" dependencies = [ - "async-recursion", "async-trait", "cfg-if", "data-encoding", @@ -6013,6 +6340,7 @@ dependencies = [ "ipnet", "once_cell", "rand 0.9.5", + "ring 0.17.14", "serde", "thiserror 2.0.21", "tinyvec", @@ -6023,9 +6351,9 @@ dependencies = [ [[package]] name = "hickory-resolver" -version = "0.25.0-alpha.5" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5762f69ebdbd4ddb2e975cd24690bf21fe6b2604039189c26acddbc427f12887" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" dependencies = [ "cfg-if", "futures-util", @@ -6217,7 +6545,9 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ + "subtle", "typenum", + "zeroize", ] [[package]] @@ -6344,7 +6674,7 @@ dependencies = [ "hyper 1.12.0", "hyper-util", "log", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-native-certs 0.8.4", "tokio", "tokio-rustls 0.26.6", @@ -6461,6 +6791,28 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_calendar" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58655df2f728e46e4eee80bddebefacf52ec3e77cdb925014b47c5a5905eb55c" +dependencies = [ + "calendrical_calculations", + "displaydoc", + "icu_calendar_data", + "icu_locale_core", + "icu_locale_fallback", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_calendar_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc00caaa3fb3201ff7a18aa458e8c3ab042b9da154cfdf948bdde3936c31c36e" + [[package]] name = "icu_collections" version = "2.3.0" @@ -6733,15 +7085,6 @@ version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" -[[package]] -name = "ipnetwork" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" -dependencies = [ - "serde", -] - [[package]] name = "is-macro" version = "0.3.8" @@ -6808,6 +7151,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "ixdtf" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3667095d64c3ecffc96463a21157b04bf3e252f6e8d5750b20c02e33c194e3" + [[package]] name = "jiff" version = "0.2.38" @@ -6887,7 +7236,7 @@ checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" dependencies = [ "proc-macro2", "quote", - "rustc_version 0.4.1", + "rustc_version", "simd_cesu8", "syn 2.0.119", ] @@ -6995,7 +7344,7 @@ dependencies = [ "pem", "serde", "serde_json", - "signature", + "signature 2.2.0", "simple_asn1", "zeroize", ] @@ -7022,6 +7371,25 @@ dependencies = [ "serde_json", ] +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "keccak" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", +] + [[package]] name = "keyed_priority_queue" version = "0.4.2" @@ -7093,7 +7461,7 @@ dependencies = [ "k8s-openapi", "kube-core", "pem", - "rustls 0.23.35", + "rustls 0.23.40", "secrecy", "serde", "serde_json", @@ -7582,7 +7950,7 @@ dependencies = [ "base64 0.22.1", "gethostname", "mail-builder", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-pki-types", "smtp-proto", "tokio", @@ -8072,6 +8440,7 @@ dependencies = [ "cfg-if", "cfg_aliases", "libc", + "memoffset", ] [[package]] @@ -8291,7 +8660,6 @@ checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", - "rand 0.8.5", "serde", ] @@ -8490,6 +8858,18 @@ dependencies = [ "web-time", ] +[[package]] +name = "ocb3" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c196e0276c471c843dd5777e7543a36a298a4be942a2a688d8111cd43390dedb" +dependencies = [ + "aead", + "cipher 0.4.4", + "ctr", + "subtle", +] + [[package]] name = "odpic-sys" version = "0.1.1" @@ -8499,6 +8879,15 @@ dependencies = [ "cc", ] +[[package]] +name = "oid-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +dependencies = [ + "asn1-rs 0.5.2", +] + [[package]] name = "oid-registry" version = "0.7.1" @@ -8528,6 +8917,10 @@ name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "once_cell_polyfill" @@ -8667,23 +9060,23 @@ dependencies = [ [[package]] name = "opentelemetry" -version = "0.27.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab70038c28ed37b97d8ed414b6429d343a8bbf44c9f79ec854f3a643029ba6d7" +checksum = "aaf416e4cb72756655126f7dd7bb0af49c674f4c1b9903e80c009e0c37e552e6" dependencies = [ "futures-core", "futures-sink", "js-sys", "pin-project-lite", - "thiserror 1.0.69", + "thiserror 2.0.21", "tracing", ] [[package]] name = "opentelemetry" -version = "0.30.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf416e4cb72756655126f7dd7bb0af49c674f4c1b9903e80c009e0c37e552e6" +checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682" dependencies = [ "futures-core", "futures-sink", @@ -8705,18 +9098,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "opentelemetry-http" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a8a7f5f6ba7c1b286c2fbca0454eaba116f63bbe69ed250b642d36fbb04d80" -dependencies = [ - "async-trait", - "bytes", - "http 1.5.0", - "opentelemetry 0.27.1", -] - [[package]] name = "opentelemetry-http" version = "0.30.0" @@ -8731,24 +9112,15 @@ dependencies = [ ] [[package]] -name = "opentelemetry-otlp" -version = "0.27.0" +name = "opentelemetry-http" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91cf61a1868dacc576bf2b2a1c3e9ab150af7272909e80085c3173384fe11f76" +checksum = "5683015d09e2df236ef005b17f6f196f0d5f6313c4fa43a7b6a53b52776e4331" dependencies = [ "async-trait", - "futures-core", + "bytes", "http 1.5.0", - "opentelemetry 0.27.1", - "opentelemetry-http 0.27.0", - "opentelemetry-proto 0.27.0", - "opentelemetry_sdk 0.27.1", - "prost", - "serde_json", - "thiserror 1.0.69", - "tokio", - "tonic 0.12.3", - "tracing", + "opentelemetry 0.32.0", ] [[package]] @@ -8762,7 +9134,7 @@ dependencies = [ "opentelemetry-http 0.30.0", "opentelemetry-proto 0.30.0", "opentelemetry_sdk 0.30.0", - "prost", + "prost 0.13.5", "reqwest 0.12.28", "thiserror 2.0.21", "tokio", @@ -8771,17 +9143,19 @@ dependencies = [ ] [[package]] -name = "opentelemetry-proto" -version = "0.27.0" +name = "opentelemetry-otlp" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e05acbfada5ec79023c85368af14abd0b307c015e9064d249b2a950ef459a6" +checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35" dependencies = [ - "hex", - "opentelemetry 0.27.1", - "opentelemetry_sdk 0.27.1", - "prost", - "serde", - "tonic 0.12.3", + "http 1.5.0", + "opentelemetry 0.32.0", + "opentelemetry-http 0.32.0", + "opentelemetry-proto 0.32.0", + "opentelemetry_sdk 0.32.1", + "prost 0.14.4", + "serde_json", + "thiserror 2.0.21", ] [[package]] @@ -8794,16 +9168,24 @@ dependencies = [ "hex", "opentelemetry 0.30.0", "opentelemetry_sdk 0.30.0", - "prost", + "prost 0.13.5", "serde", "tonic 0.13.1", ] [[package]] -name = "opentelemetry-semantic-conventions" -version = "0.27.0" +name = "opentelemetry-proto" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc1b6902ff63b32ef6c489e8048c5e253e2e4a803ea3ea7e783914536eb15c52" +checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" +dependencies = [ + "base64 0.22.1", + "const-hex", + "opentelemetry 0.32.0", + "opentelemetry_sdk 0.32.1", + "prost 0.14.4", + "serde", +] [[package]] name = "opentelemetry-semantic-conventions" @@ -8812,25 +9194,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83d059a296a47436748557a353c5e6c5705b9470ef6c95cfc52c21a8814ddac2" [[package]] -name = "opentelemetry_sdk" -version = "0.27.1" +name = "opentelemetry-semantic-conventions" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "231e9d6ceef9b0b2546ddf52335785ce41252bc7474ee8ba05bfad277be13ab8" -dependencies = [ - "async-trait", - "futures-channel", - "futures-executor", - "futures-util", - "glob", - "opentelemetry 0.27.1", - "percent-encoding", - "rand 0.8.5", - "serde_json", - "thiserror 1.0.69", - "tokio", - "tokio-stream", - "tracing", -] +checksum = "c913ac17a6c451661ee255f4625d143e51647ae78ebd969b75e41c4442f4fe47" [[package]] name = "opentelemetry_sdk" @@ -8850,6 +9217,24 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "opentelemetry_sdk" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b59f80e1ac4d5ff7a2db8fb6c80badb7f0f3f858211fba08dd9aaec750894f9" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry 0.32.0", + "percent-encoding", + "portable-atomic", + "rand 0.9.5", + "thiserror 2.0.21", + "tokio", + "tokio-stream", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -8939,7 +9324,7 @@ checksum = "3cae83056e7cb770211494a0ecf66d9fa7eba7d00977e5bb91f0e925b40b937f" dependencies = [ "cbc", "cms", - "der", + "der 0.7.10", "des 0.8.1", "hex", "hmac 0.12.1", @@ -8960,7 +9345,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ "ecdsa", - "elliptic-curve", + "elliptic-curve 0.13.8", "primeorder", "sha2 0.10.9", ] @@ -8972,7 +9357,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" dependencies = [ "ecdsa", - "elliptic-curve", + "elliptic-curve 0.13.8", "primeorder", "sha2 0.10.9", ] @@ -8983,9 +9368,9 @@ version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" dependencies = [ - "base16ct", + "base16ct 0.2.0", "ecdsa", - "elliptic-curve", + "elliptic-curve 0.13.8", "primeorder", "rand_core 0.6.4", "sha2 0.10.9", @@ -9224,7 +9609,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros", + "phf_macros 0.11.3", "phf_shared 0.11.3", ] @@ -9243,6 +9628,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ + "phf_macros 0.13.1", "phf_shared 0.13.1", "serde", ] @@ -9253,7 +9639,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ - "phf_generator", + "phf_generator 0.11.3", "phf_shared 0.11.3", ] @@ -9267,19 +9653,42 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand 2.5.0", + "phf_shared 0.13.1", +] + [[package]] name = "phf_macros" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ - "phf_generator", + "phf_generator 0.11.3", "phf_shared 0.11.3", "proc-macro2", "quote", "syn 2.0.119", ] +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "phf_shared" version = "0.11.3" @@ -9380,9 +9789,9 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der", - "pkcs8", - "spki", + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", ] [[package]] @@ -9393,9 +9802,9 @@ checksum = "695b3df3d3cc1015f12d70235e35b6b79befc5fa7a9b95b951eab1dd07c9efc2" dependencies = [ "cms", "const-oid 0.9.6", - "der", + "der 0.7.10", "digest 0.10.7", - "spki", + "spki 0.7.3", "x509-cert", "zeroize", ] @@ -9408,11 +9817,11 @@ checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" dependencies = [ "aes 0.8.3", "cbc", - "der", + "der 0.7.10", "pbkdf2", "scrypt", "sha2 0.10.9", - "spki", + "spki 0.7.3", ] [[package]] @@ -9421,8 +9830,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.2", + "spki 0.8.1", ] [[package]] @@ -9608,7 +10027,7 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" dependencies = [ - "elliptic-curve", + "elliptic-curve 0.13.8", ] [[package]] @@ -9643,29 +10062,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-rules" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c277e4e643ef00c1233393c673f655e3672cf7eb3ba08a00bdd0ea59139b5f" -dependencies = [ - "proc-macro-rules-macros", - "proc-macro2", - "syn 2.0.119", -] - -[[package]] -name = "proc-macro-rules-macros" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "207fffb0fe655d1d47f6af98cc2793405e85929bdbc420d685554ff07be27ac7" -dependencies = [ - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "proc-macro-utils" version = "0.10.0" @@ -9739,6 +10135,21 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bitflags 2.13.2", + "num-traits", + "rand 0.9.5", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax 0.8.11", + "unarray", +] + [[package]] name = "prost" version = "0.13.5" @@ -9746,7 +10157,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.13.5", +] + +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive 0.14.4", ] [[package]] @@ -9762,13 +10183,26 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "prost-types" version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" dependencies = [ - "prost", + "prost 0.13.5", ] [[package]] @@ -9886,7 +10320,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.3", - "rustls 0.23.35", + "rustls 0.23.40", "socket2 0.6.5", "thiserror 2.0.21", "tokio", @@ -9902,13 +10336,14 @@ checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" dependencies = [ "aws-lc-rs", "bytes", + "fastbloom", "getrandom 0.4.3", "lru-slab", "rand 0.10.3", "rand_pcg", "ring 0.17.14", "rustc-hash 2.1.3", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-pki-types", "slab", "thiserror 2.0.21", @@ -10094,6 +10529,15 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + [[package]] name = "raw-cpuid" version = "11.6.0" @@ -10373,7 +10817,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-native-certs 0.8.4", "rustls-pki-types", "serde", @@ -10420,7 +10864,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-pki-types", "rustls-platform-verifier", "serde", @@ -10476,6 +10920,16 @@ dependencies = [ "wasmtimer", ] +[[package]] +name = "resb" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22d392791f3c6802a1905a509e9d1a6039cbbcb5e9e00e5a6d3661f7c874f390" +dependencies = [ + "potential_utf", + "serde_core", +] + [[package]] name = "resolv-conf" version = "0.7.6" @@ -10666,10 +11120,10 @@ dependencies = [ "num-integer", "num-traits", "pkcs1", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", - "signature", - "spki", + "signature 2.2.0", + "spki 0.7.3", "subtle", "zeroize", ] @@ -10765,22 +11219,13 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" -[[package]] -name = "rustc_version" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -dependencies = [ - "semver 0.9.0", -] - [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.28", + "semver", ] [[package]] @@ -10860,9 +11305,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", "log", @@ -10881,7 +11326,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70cc376c6ba1823ae229bacf8ad93c136d93524eab0e4e5e0e4f96b9c4e5b212" dependencies = [ "log", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-native-certs 0.7.3", "rustls-pki-types", "rustls-webpki 0.103.15", @@ -10963,7 +11408,7 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-native-certs 0.8.4", "rustls-platform-verifier-android", "rustls-webpki 0.103.15", @@ -10981,12 +11426,13 @@ checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" [[package]] name = "rustls-tokio-stream" -version = "0.5.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faa7dc7c991d9164e55bbf1558029eb5b84d32cc4d61a7df5b8641b2deedc4b3" +checksum = "0560d12c0d8c672f849197de91b9ee61f5bf9aa024c97aaeeb112ec2f6c347fd" dependencies = [ + "derive-io", "futures", - "rustls 0.23.35", + "rustls 0.23.40", "socket2 0.5.10", "tokio", ] @@ -11288,11 +11734,25 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct", - "der", + "base16ct 0.2.0", + "der 0.7.10", "generic-array", - "pkcs8", - "serdect", + "pkcs8 0.10.2", + "serdect 0.2.0", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct 1.0.0", + "ctutils", + "der 0.8.2", + "hybrid-array", "subtle", "zeroize", ] @@ -11342,27 +11802,12 @@ dependencies = [ "libc", ] -[[package]] -name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] - [[package]] name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" - [[package]] name = "seq-macro" version = "0.3.6" @@ -11541,16 +11986,16 @@ dependencies = [ [[package]] name = "serde_v8" -version = "0.261.1" +version = "0.321.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3495190857461e87a2716141043218aad5281f219f54a03b7ebbe605b3b931df" +checksum = "fd3b6f6140192f569b88997e2ea7afeef6aa38b64374e84784ea1d1ea35679d3" dependencies = [ - "deno_error 0.6.1", + "deno_error", + "deno_v8", "num-bigint", "serde", "smallvec", "thiserror 2.0.21", - "v8", ] [[package]] @@ -11620,7 +12065,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" dependencies = [ - "base16ct", + "base16ct 0.2.0", + "serde", +] + +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct 1.0.0", "serde", ] @@ -11695,6 +12150,27 @@ dependencies = [ "digest 0.11.3", ] +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak 0.1.6", +] + +[[package]] +name = "shake" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.2", + "sponge-cursor", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -11735,6 +12211,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -11751,9 +12237,9 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1e303f8205714074f6068773f0e29527e0453937fe837c9717d066635b65f31" dependencies = [ - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", - "signature", + "signature 2.2.0", "zeroize", ] @@ -11767,6 +12253,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + [[package]] name = "simd-adler32" version = "0.3.10" @@ -11779,7 +12275,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" dependencies = [ - "rustc_version 0.4.1", + "rustc_version", "simdutf8", ] @@ -11947,7 +12443,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef958a98b9d5da290cfc78946e9f3e61e1e62a18db0d92cac0b83cc161491a9" +dependencies = [ + "base64ct", + "der 0.8.2", ] [[package]] @@ -11962,6 +12468,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + [[package]] name = "sql-builder" version = "3.1.1" @@ -12041,7 +12553,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rustls 0.23.35", + "rustls 0.23.40", "serde", "serde_json", "sha2 0.10.9", @@ -12238,6 +12750,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strck" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42316e70da376f3d113a68d138a60d8a9883c604fe97942721ec2068dab13a9f" +dependencies = [ + "unicode-ident", +] + [[package]] name = "string_enum" version = "1.0.2" @@ -12751,6 +13272,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn-match" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b8f0a9004d6aafa6a588602a1119e6cdaacec9921aa1605383e6e7d6258fd6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -12760,6 +13292,18 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -12793,11 +13337,14 @@ dependencies = [ [[package]] name = "sys_traits" -version = "0.1.16" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4707edf3196e8037ee45018d1bb1bfb233b0e4fc440fa3d3f25bc69bfdaf26" +checksum = "c7a35f11f9773f11930a7d1e6276766f1577400ee2530b7f0cf3844c2dcbf12b" dependencies = [ + "junction", + "libc", "sys_traits_macros", + "windows-sys 0.59.0", ] [[package]] @@ -13052,15 +13599,6 @@ dependencies = [ "rustls-pemfile 2.2.0", ] -[[package]] -name = "temp_deno_which" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "366c5ccd670145885feb6efd6bbf2478ed236c4c3839046fcc8e2a1a84c51091" -dependencies = [ - "either", -] - [[package]] name = "tempfile" version = "3.27.0" @@ -13074,6 +13612,40 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "temporal_capi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f48eb9ca4ca22caaa48d1e7a534ad3fd598a23d1a71945e8ad5ac0be501fa5f2" +dependencies = [ + "diplomat", + "diplomat-runtime", + "icu_calendar", + "icu_locale_core", + "num-traits", + "temporal_rs", + "timezone_provider", + "writeable", + "zoneinfo64", +] + +[[package]] +name = "temporal_rs" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a978fef907a27736827d336c0bfaab14fb8a7509dd6dc2b43c44e51d59aca5b" +dependencies = [ + "calendrical_calculations", + "core_maths", + "icu_calendar", + "icu_locale_core", + "ixdtf", + "num-traits", + "timezone_provider", + "tinystr", + "writeable", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -13274,6 +13846,18 @@ dependencies = [ "time-core", ] +[[package]] +name = "timezone_provider" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ec4f4eebb4817fde02a411aedc7ac5f66c89c68c49e5d4b17a8139f077af52" +dependencies = [ + "tinystr", + "zerotrie", + "zerovec", + "zoneinfo64", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -13350,9 +13934,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.46.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", @@ -13363,10 +13947,10 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "slab", - "socket2 0.5.10", + "socket2 0.6.5", "tokio-macros", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -13490,7 +14074,7 @@ version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" dependencies = [ - "rustls 0.23.35", + "rustls 0.23.40", "tokio", ] @@ -13669,13 +14253,10 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" dependencies = [ - "async-stream", "async-trait", - "axum 0.7.9", "base64 0.22.1", "bytes", "flate2", - "h2 0.4.20", "http 1.5.0", "http-body 1.1.0", "http-body-util", @@ -13684,9 +14265,8 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "prost", + "prost 0.13.5", "rustls-pemfile 2.2.0", - "socket2 0.5.10", "tokio", "tokio-rustls 0.26.6", "tokio-stream", @@ -13704,7 +14284,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" dependencies = [ "async-trait", - "axum 0.8.9", + "axum", "base64 0.22.1", "bytes", "flate2", @@ -13717,7 +14297,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "prost", + "prost 0.13.5", "rustls-native-certs 0.8.4", "socket2 0.5.10", "tokio", @@ -13775,7 +14355,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36" dependencies = [ - "axum-core 0.5.6", + "axum-core", "cookie", "futures-util", "http 1.5.0", @@ -14129,6 +14709,12 @@ dependencies = [ "web-time", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unic-char-property" version = "0.9.0" @@ -14344,7 +14930,7 @@ dependencies = [ "log", "native-tls", "once_cell", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-pki-types", "serde", "serde_json", @@ -14374,13 +14960,13 @@ checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "urlpattern" -version = "0.3.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +checksum = "0f805818f843b548bacc19609eb3619dd2850e54746f5cada37927393c2ef4ec" dependencies = [ + "icu_properties", "regex", "serde", - "unic-ucd-ident", "url", ] @@ -14428,20 +15014,36 @@ dependencies = [ [[package]] name = "v8" -version = "137.3.0" +version = "150.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33995a1fee055ff743281cde33a41f0d618ee0bdbe8bdf6859e11864499c2595" +checksum = "42a978ff11f15b24e5c05a7123cf2b68f41e763546699781a924ef4e2cf43a49" dependencies = [ - "bindgen 0.71.1", + "bindgen 0.72.1", "bitflags 2.13.2", "fslock", "gzip-header", "home", "miniz_oxide 0.8.9", "paste", + "temporal_capi", "which 6.0.3", ] +[[package]] +name = "v8x" +version = "149.4.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3701ffb3692a24808503d17f758b0a3f688d2c451779247982566b5941033e63" +dependencies = [ + "bindgen 0.70.1", + "bitflags 2.13.2", + "cc", + "paste", + "temporal_capi", + "temporal_rs", + "timezone_provider", +] + [[package]] name = "valuable" version = "0.1.1" @@ -14661,11 +15263,11 @@ dependencies = [ [[package]] name = "wasm_dep_analyzer" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51cf5f08b357e64cd7642ab4bbeb11aecab9e15520692129624fb9908b8df2c" +checksum = "a10e6b67c951a84de7029487e0e0a496860dae49f6699edd279d5ff35b8fbf54" dependencies = [ - "deno_error 0.6.1", + "deno_error", "thiserror 2.0.21", ] @@ -14766,6 +15368,12 @@ dependencies = [ "winsafe", ] +[[package]] +name = "which" +version = "8.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae2f2b2b816647a1cab1acc91f5bd20812d53cb344382635ec2181940c8034f" + [[package]] name = "whoami" version = "1.6.1" @@ -14836,7 +15444,7 @@ dependencies = [ "aws-config", "aws-credential-types", "aws-sdk-sqs", - "axum 0.8.9", + "axum", "base64 0.22.1", "chrono", "constant_time_eq 0.3.1", @@ -14856,7 +15464,7 @@ dependencies = [ "rdkafka", "reqwest 0.13.5", "rumqttc", - "rustls 0.23.35", + "rustls 0.23.40", "serde", "serde_derive", "serde_json", @@ -14955,7 +15563,7 @@ dependencies = [ name = "windmill-alerting" version = "1.830.0" dependencies = [ - "axum 0.8.9", + "axum", "chrono", "serde", "serde_json", @@ -14977,7 +15585,7 @@ dependencies = [ "async_zip", "aws-sdk-config", "aws-sigv4", - "axum 0.8.9", + "axum", "base32", "base64 0.22.1", "bytes", @@ -15019,7 +15627,7 @@ dependencies = [ "reqwest 0.13.5", "rsa", "rust-embed", - "rustls 0.23.35", + "rustls 0.23.40", "samael", "serde", "serde_json", @@ -15108,7 +15716,7 @@ dependencies = [ name = "windmill-api-agent-workers" version = "1.830.0" dependencies = [ - "axum 0.8.9", + "axum", "chrono", "http 1.5.0", "hyper 1.12.0", @@ -15131,7 +15739,7 @@ dependencies = [ name = "windmill-api-assets" version = "1.830.0" dependencies = [ - "axum 0.8.9", + "axum", "chrono", "serde", "serde_json", @@ -15149,7 +15757,7 @@ name = "windmill-api-auth" version = "1.830.0" dependencies = [ "anyhow", - "axum 0.8.9", + "axum", "chrono", "http 1.5.0", "itertools 0.14.0", @@ -15184,7 +15792,7 @@ dependencies = [ name = "windmill-api-configs" version = "1.830.0" dependencies = [ - "axum 0.8.9", + "axum", "chrono", "itertools 0.14.0", "serde", @@ -15201,7 +15809,7 @@ dependencies = [ name = "windmill-api-debug" version = "1.830.0" dependencies = [ - "axum 0.8.9", + "axum", "base64 0.22.1", "chrono", "ed25519-dalek", @@ -15224,7 +15832,7 @@ name = "windmill-api-embeddings" version = "1.830.0" dependencies = [ "anyhow", - "axum 0.8.9", + "axum", "candle-core", "candle-nn", "candle-transformers", @@ -15246,7 +15854,7 @@ dependencies = [ name = "windmill-api-flow-conversations" version = "1.830.0" dependencies = [ - "axum 0.8.9", + "axum", "chrono", "serde", "sql-builder", @@ -15262,7 +15870,7 @@ dependencies = [ name = "windmill-api-flows" version = "1.830.0" dependencies = [ - "axum 0.8.9", + "axum", "chrono", "hyper 1.12.0", "serde", @@ -15284,7 +15892,7 @@ dependencies = [ name = "windmill-api-groups" version = "1.830.0" dependencies = [ - "axum 0.8.9", + "axum", "chrono", "globset", "lazy_static", @@ -15306,7 +15914,7 @@ dependencies = [ name = "windmill-api-inputs" version = "1.830.0" dependencies = [ - "axum 0.8.9", + "axum", "chrono", "serde", "serde_json", @@ -15325,7 +15933,7 @@ dependencies = [ "aws-config", "aws-credential-types", "aws-sdk-sqs", - "axum 0.8.9", + "axum", "base64 0.22.1", "futures", "hex", @@ -15357,7 +15965,7 @@ name = "windmill-api-jobs" version = "1.830.0" dependencies = [ "anyhow", - "axum 0.8.9", + "axum", "base64 0.22.1", "chrono", "http 1.5.0", @@ -15382,7 +15990,7 @@ dependencies = [ name = "windmill-api-npm-proxy" version = "1.830.0" dependencies = [ - "axum 0.8.9", + "axum", "chrono", "flate2", "futures", @@ -15390,7 +15998,7 @@ dependencies = [ "lazy_static", "quick_cache", "reqwest 0.13.5", - "semver 1.0.28", + "semver", "serde", "serde_json", "sha2 0.10.9", @@ -15411,7 +16019,7 @@ name = "windmill-api-openapi" version = "1.830.0" dependencies = [ "anyhow", - "axum 0.8.9", + "axum", "http 1.5.0", "indexmap 2.14.2", "itertools 0.14.0", @@ -15432,7 +16040,7 @@ dependencies = [ name = "windmill-api-schedule" version = "1.830.0" dependencies = [ - "axum 0.8.9", + "axum", "chrono", "chrono-tz", "serde", @@ -15453,7 +16061,7 @@ name = "windmill-api-scripts" version = "1.830.0" dependencies = [ "anyhow", - "axum 0.8.9", + "axum", "chrono", "futures", "http 1.5.0", @@ -15495,7 +16103,7 @@ name = "windmill-api-settings" version = "1.830.0" dependencies = [ "anyhow", - "axum 0.8.9", + "axum", "base64 0.22.1", "bytes", "chrono", @@ -15536,7 +16144,7 @@ name = "windmill-api-users" version = "1.830.0" dependencies = [ "argon2", - "axum 0.8.9", + "axum", "chrono", "http 1.5.0", "hyper 1.12.0", @@ -15559,7 +16167,7 @@ dependencies = [ name = "windmill-api-workers" version = "1.830.0" dependencies = [ - "axum 0.8.9", + "axum", "chrono", "serde", "serde_json", @@ -15573,7 +16181,7 @@ dependencies = [ name = "windmill-api-workspaces" version = "1.830.0" dependencies = [ - "axum 0.8.9", + "axum", "chrono", "constant_time_eq 0.3.1", "futures", @@ -15628,7 +16236,7 @@ name = "windmill-autoscaling" version = "1.830.0" dependencies = [ "anyhow", - "axum 0.8.9", + "axum", "k8s-openapi", "kube", "serde", @@ -15658,7 +16266,7 @@ dependencies = [ "aws-sdk-secretsmanager", "aws-sdk-sts", "aws-smithy-types-convert", - "axum 0.8.9", + "axum", "backon", "base64 0.22.1", "bitflags 2.13.2", @@ -15712,18 +16320,18 @@ dependencies = [ "reqwest-middleware", "reqwest-retry", "rsa", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-native-certs 0.8.4", "ryu-js", "schemars 0.8.22", - "semver 1.0.28", + "semver", "serde", "serde_json", "serde_yml", "sha1", "sha2 0.10.9", "size", - "spki", + "spki 0.7.3", "sqlx", "strum", "strum_macros", @@ -15880,7 +16488,7 @@ version = "1.830.0" dependencies = [ "anyhow", "async-trait", - "axum 0.8.9", + "axum", "backon", "base64 0.22.1", "chrono", @@ -15912,7 +16520,7 @@ dependencies = [ "anyhow", "arc-swap", "async-oauth2", - "axum 0.8.9", + "axum", "base64 0.22.1", "chrono", "hex", @@ -15941,7 +16549,7 @@ dependencies = [ "aws-credential-types", "aws-sdk-sts", "aws-smithy-types-convert", - "axum 0.8.9", + "axum", "bytes", "chrono", "datafusion", @@ -16258,7 +16866,7 @@ version = "1.830.0" dependencies = [ "anyhow", "async-recursion", - "axum 0.8.9", + "axum", "backon", "chrono", "chrono-tz", @@ -16298,18 +16906,14 @@ dependencies = [ "anyhow", "const_format", "deno_ast", - "deno_console", "deno_core", "deno_crypto", - "deno_error 0.6.1", + "deno_error", "deno_fetch", - "deno_fs", - "deno_io", "deno_net", "deno_permissions", "deno_telemetry", "deno_tls", - "deno_url", "deno_web", "deno_webidl", "futures", @@ -16318,13 +16922,15 @@ dependencies = [ "rcgen", "regex", "reqwest 0.13.5", - "rustls 0.23.35", + "rustls 0.23.40", "serde", "serde_json", "sqlx", + "sys_traits", "tokio", "tracing", "uuid", + "v8", "winapi", "windmill-common", "windmill-parser-ts", @@ -16348,7 +16954,7 @@ version = "1.830.0" dependencies = [ "anyhow", "async-recursion", - "axum 0.8.9", + "axum", "base64 0.22.1", "chrono", "dashmap", @@ -16383,7 +16989,7 @@ version = "1.830.0" dependencies = [ "anyhow", "async-trait", - "axum 0.8.9", + "axum", "chrono", "futures", "serde", @@ -16407,7 +17013,7 @@ version = "1.830.0" dependencies = [ "anyhow", "async-trait", - "axum 0.8.9", + "axum", "chrono", "http 1.5.0", "hyper 1.12.0", @@ -16440,7 +17046,7 @@ version = "1.830.0" dependencies = [ "anyhow", "async-trait", - "axum 0.8.9", + "axum", "base64 0.22.1", "futures", "itertools 0.14.0", @@ -16467,7 +17073,7 @@ version = "1.830.0" dependencies = [ "anyhow", "async-trait", - "axum 0.8.9", + "axum", "base64 0.22.1", "bytes", "chrono", @@ -16500,7 +17106,7 @@ version = "1.830.0" dependencies = [ "anyhow", "async-trait", - "axum 0.8.9", + "axum", "base64 0.22.1", "lazy_static", "regex", @@ -16520,7 +17126,7 @@ version = "1.830.0" dependencies = [ "anyhow", "async-trait", - "axum 0.8.9", + "axum", "base64 0.22.1", "bytes", "chrono", @@ -16554,7 +17160,7 @@ version = "1.830.0" dependencies = [ "anyhow", "async-trait", - "axum 0.8.9", + "axum", "base64 0.22.1", "chrono", "constant_time_eq 0.3.1", @@ -16591,7 +17197,7 @@ version = "1.830.0" dependencies = [ "anyhow", "async-trait", - "axum 0.8.9", + "axum", "base64 0.22.1", "itertools 0.14.0", "rdkafka", @@ -16614,7 +17220,7 @@ version = "1.830.0" dependencies = [ "anyhow", "async-trait", - "axum 0.8.9", + "axum", "base64 0.22.1", "bytes", "itertools 0.14.0", @@ -16639,7 +17245,7 @@ dependencies = [ "anyhow", "async-nats", "async-trait", - "axum 0.8.9", + "axum", "base64 0.22.1", "itertools 0.14.0", "nkeys", @@ -16662,7 +17268,7 @@ version = "1.830.0" dependencies = [ "anyhow", "async-trait", - "axum 0.8.9", + "axum", "byteorder", "bytes", "chrono", @@ -16702,7 +17308,7 @@ dependencies = [ "aws-sdk-sqs", "aws-sdk-sts", "aws-smithy-types", - "axum 0.8.9", + "axum", "backon", "chrono", "itertools 0.14.0", @@ -16725,7 +17331,7 @@ version = "1.830.0" dependencies = [ "anyhow", "async-trait", - "axum 0.8.9", + "axum", "base64 0.22.1", "futures", "http 1.5.0", @@ -16772,7 +17378,7 @@ dependencies = [ "async-recursion", "async-stream", "async-trait", - "axum 0.8.9", + "axum", "backon", "base64 0.22.1", "bit-vec 0.6.3", @@ -16817,7 +17423,7 @@ dependencies = [ "postgres-protocol", "process-wrap", "prometheus", - "prost", + "prost 0.13.5", "quick_cache", "rand 0.9.5", "rcgen", @@ -16826,7 +17432,7 @@ dependencies = [ "reqwest-middleware", "rsa", "rust_decimal", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-native-certs 0.8.4", "rustls-pemfile 2.2.0", "serde", @@ -17480,8 +18086,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" dependencies = [ "const-oid 0.9.6", - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "x509-parser" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7069fba5b66b9193bd2c5d3d4ff12b839118f6bcbef5328efafafb5395cf63da" +dependencies = [ + "asn1-rs 0.5.2", + "data-encoding", + "der-parser 8.2.0", + "lazy_static", + "nom", + "oid-registry 0.6.1", + "rusticata-macros", + "thiserror 1.0.69", + "time", ] [[package]] @@ -17711,6 +18334,19 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" +[[package]] +name = "zoneinfo64" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6eb2607e906160c457fd573e9297e65029669906b9ac8fb1b5cd5e055f0705" +dependencies = [ + "calendrical_calculations", + "icu_locale_core", + "potential_utf", + "resb", + "serde", +] + [[package]] name = "zstd" version = "0.13.3" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index f50a858d52..e91ad01795 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -451,7 +451,7 @@ headers = "^0" hyper = { version = "^1", features = ["full"] } hyper-tls = "^0.6" hyper-util = { version = "^0.1", features = ["client-legacy", "http1", "tokio"] } -tokio = { version = "=1.46.1", features = ["full", "tracing", "time"] } +tokio = { version = "=1.47.1", features = ["full", "tracing", "time"] } tokio-stream = { version = "0.1.17" } tower = "^0" tower-http = { version = "^0.6", features = ["trace", "cors", "catch-panic"] } @@ -519,33 +519,29 @@ aws-sdk-rds = "^1" async-trait = "0.1.88" -v8 = "=137.1.0" # Exact version NOTE: Do not forget to update version and hash in flake.nix -# deno_* pin set: deno v2.4.0 base, with deno_ast force-overridden to =0.51.0. -# Rationale: deno_ast 0.51.0 is the first version pulling swc_common =14.0.4, -# the first swc_common patch that dropped `pub use serde::__private as serde;` -# (the line that capped our workspace serde pin at =1.0.220). v2.4.0's other -# pins keep deno_tls at 0.196.0 which uses permissive `rustls ^0.23.11`, -# compatible with aws-sdk-bedrockruntime's `^0.23.31` requirement. deno_tls -# 0.198+ tightened that to exact `=0.23.28`, which would have made any -# meaningful deno bump resolver-impossible against aws-sdk. -deno_fetch = "0.233.0" -deno_tls = "0.196.0" -deno_console = "0.209.0" -deno_url = "0.209.0" -deno_webidl = "0.209.0" -deno_web = "0.240.0" -# Sibling release of the pinned deno_core 0.352 / deno_web 0.240 stack -# (its deps are deno_core ^0.352, deno_web ^0.240, deno_error =0.6.1). A newer -# deno_crypto would force bumping the whole deno stack. -deno_crypto = "0.223.0" -deno_io = "0.119.0" -deno_fs = "0.119.0" -deno_net = "0.201.0" -deno_core = "0.352.0" +# deno_* pin set: the crates deno published together on 2026-09-16 (deno_core +# 0.412). +# V8 is pinned exactly because flake.nix fetches the prebuilt archive of that +# version: update its version and hash there with any bump. No default features, +# so the pin leaves the feature set deno_core picks untouched. +v8 = { version = "=150.4.0", default-features = false } +# These release as a matched set: each pins its siblings, deno_error exactly, +# and deno_tls pins rustls exactly (=0.23.40, the workspace `rustls` pin below +# must equal it). A bump moves all of them at once. +# deno_ast is independent of that set and stays at 0.51.0: a newer one pulls +# swc_common 17, which would force bumping every swc_* pin the parsers use. +deno_fetch = "0.283.0" +deno_tls = "0.246.0" +deno_webidl = "0.259.0" +deno_web = "0.290.0" +deno_crypto = "0.273.0" +deno_net = "0.251.0" +deno_core = "0.412.0" deno_ast = { version = "=0.51.0", features = ["transpiling"] } -deno_permissions = "0.68.0" -deno_telemetry = "0.31.0" -deno_error = "=0.6.1" +deno_permissions = "0.118.0" +deno_telemetry = "0.81.0" +deno_error = "=0.7.1" +sys_traits = { version = "0.1.28", features = ["real", "libc", "winapi"] } rustls-pemfile = "2.2.0" # only used with special deno_core_mac feature to prevent ffi issue on macos, requires libffi to be installed @@ -682,7 +678,7 @@ aws-sdk-sqs = "^1.89.0" aws-sdk-sts = "^1.91.0" aws-sdk-sso = "=1.77.0" aws-sdk-ssooidc = "=1.78.0" -rustls = "=0.23.35" +rustls = "=0.23.40" async-once-cell = "0.5.4" size = "0.5.0" diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 15e23c383d..b2d35dc125 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -5d7726c4979070ee8db4fb9e8014152872a641ba +599eae0034b34c5e0d3d42df37cf18bbe01f9b49 diff --git a/backend/windmill-runtime-nativets/Cargo.toml b/backend/windmill-runtime-nativets/Cargo.toml index d0b4e97f19..22da97ab1d 100644 --- a/backend/windmill-runtime-nativets/Cargo.toml +++ b/backend/windmill-runtime-nativets/Cargo.toml @@ -22,16 +22,15 @@ deno_webidl.workspace = true deno_web.workspace = true deno_crypto.workspace = true deno_net.workspace = true -deno_console.workspace = true -deno_url.workspace = true deno_core.workspace = true deno_ast.workspace = true deno_tls.workspace = true deno_permissions.workspace = true -deno_io.workspace = true -deno_fs.workspace = true deno_telemetry.workspace = true deno_error.workspace = true +sys_traits.workspace = true +# Not used directly: depending on it is what makes the workspace pin bind. +v8.workspace = true winapi.workspace = true itertools.workspace = true @@ -58,14 +57,8 @@ deno_webidl.workspace = true deno_web.workspace = true deno_crypto.workspace = true deno_net.workspace = true -deno_console.workspace = true -deno_url.workspace = true deno_core.workspace = true deno_ast.workspace = true -deno_tls.workspace = true -deno_permissions.workspace = true -deno_io.workspace = true -deno_fs.workspace = true deno_telemetry.workspace = true deno_error.workspace = true winapi.workspace = true diff --git a/backend/windmill-runtime-nativets/build.rs b/backend/windmill-runtime-nativets/build.rs index 29a6dbf81b..fab6cbb1ed 100644 --- a/backend/windmill-runtime-nativets/build.rs +++ b/backend/windmill-runtime-nativets/build.rs @@ -1,107 +1,11 @@ use deno_ast::{MediaType, ParseParams}; -use deno_core::{ModuleCodeString, ModuleName, SourceMapData}; +use deno_core::{Extension, ModuleCodeString, ModuleName, SourceMapData}; use deno_error::JsErrorBox; -use deno_fetch::FetchPermissions; -use deno_net::NetPermissions; -use deno_web::{BlobStore, TimersPermission}; -use std::borrow::Cow; +use std::collections::HashSet; use std::env; +use std::fmt::Write as _; use std::io::Write; use std::path::{Path, PathBuf}; -use std::sync::Arc; - -pub struct PermissionsContainer; - -impl FetchPermissions for PermissionsContainer { - #[inline(always)] - fn check_net_url( - &mut self, - _url: &deno_core::url::Url, - _api_name: &str, - ) -> Result<(), deno_permissions::PermissionCheckError> { - unreachable!("snapshotting") - } - - #[inline(always)] - fn check_read<'a>( - &mut self, - _path: Cow<'a, Path>, - _api_name: &str, - _get_path: &'a dyn deno_fs::GetPath, - ) -> Result, deno_io::fs::FsError> { - unreachable!("snapshotting") - } - - #[inline(always)] - fn check_write<'a>( - &mut self, - _path: Cow<'a, Path>, - _api_name: &str, - _get_path: &'a dyn deno_fs::GetPath, - ) -> Result, deno_io::fs::FsError> { - unreachable!("snapshotting") - } - - #[inline(always)] - fn check_net_vsock( - &mut self, - _cid: u32, - _port: u32, - _api_name: &str, - ) -> Result<(), deno_permissions::PermissionCheckError> { - unreachable!("snapshotting") - } -} - -impl TimersPermission for PermissionsContainer { - #[inline(always)] - fn allow_hrtime(&mut self) -> bool { - true - } -} - -impl NetPermissions for PermissionsContainer { - fn check_read( - &mut self, - _p: &str, - _api_name: &str, - ) -> Result { - unreachable!("snapshotting") - } - - fn check_write( - &mut self, - _p: &str, - _api_name: &str, - ) -> Result { - unreachable!("snapshotting") - } - - fn check_net>( - &mut self, - _host: &(T, Option), - _api_name: &str, - ) -> Result<(), deno_permissions::PermissionCheckError> { - unreachable!("snapshotting") - } - - fn check_write_path<'a>( - &mut self, - _p: Cow<'a, Path>, - _api_name: &str, - ) -> Result, deno_permissions::PermissionCheckError> { - unreachable!("snapshotting") - } - - fn check_vsock( - &mut self, - _cid: u32, - _port: u32, - _api_name: &str, - ) -> Result<(), deno_permissions::PermissionCheckError> { - unreachable!("snapshotting") - } -} deno_core::extension!( fetch, @@ -109,38 +13,29 @@ deno_core::extension!( esm = ["src/runtime.js"], ); -// `extension_transpiler` callback for `deno_core::snapshot::create_snapshot`. +// `extension_transpiler` callback for `deno_core::snapshot::create_snapshot`, +// also applied to the residual lazy sources below. // -// Specialized to our snapshot's inputs. Of the eight deno_* extensions -// we register via `init()`, seven ship pre-built `.js` files -// in their `esm` lists (webidl/url/console/web/crypto/fetch/net) — only -// `deno_telemetry`'s `extension!` macro lists `.ts` files -// (`telemetry.ts`, `util.ts`), so the TypeScript branch is needed -// solely for that crate. Our local `fetch` extension contributes -// `src/runtime.js` (pure JS). No `node:` imports happen at snapshot -// build time, no `.mjs`, no user-supplied modules. So: +// Specialized to our inputs: the deno_* extensions ship `.js`, except +// `deno_telemetry` whose lazy scripts are `.ts`. Our local `fetch` extension +// contributes `src/runtime.js`. No `node:` imports, no `.mjs`, no +// user-supplied modules. So: // - `.js` → pass through. // - `.ts` → transpile via deno_ast (deno_telemetry only). // - anything else → build bug (deno shipping an unexpected file type // or us mislabelling one), panic loudly rather than emit a broken // snapshot. // -// No source maps: the snapshot is a binary blob the runtime loads — source -// maps would never be consumed. +// No source maps: nothing consumes them. // -// The signature still returns `Result<_, JsErrorBox>` because that's what -// `extension_transpiler` expects, but we never construct one — parse and -// transpile failures are build-time bugs in deno's own .ts internals (or -// in our runtime.js, if we ever change its extension), so they panic. +// The signature returns `Result<_, JsErrorBox>` because that's what +// `extension_transpiler` expects, but we never construct one: parse and +// transpile failures are build-time bugs, so they panic. // -// This replaces a call to `deno_runtime::transpile::maybe_transpile_source` -// from `deno_runtime 0.198.0`. The original is more general (handles -// `node:` modules, `.mjs`, emits source maps in debug builds, plumbs -// errors via `JsErrorBox`); none of that surface is reachable in our -// build. Dropping the `deno_runtime` dep eliminates a -// `deno_cache → rusqlite → libsqlite3-sys 0.35` transitive chain that -// collides with sqlx-sqlite's `libsqlite3-sys 0.30` (cargo's -// `links = "sqlite3"` rule). +// `deno_runtime::transpile::maybe_transpile_source` is the general version; +// depending on `deno_runtime` would pull a +// `deno_cache → rusqlite → libsqlite3-sys` chain that collides with +// sqlx-sqlite's `libsqlite3-sys` (cargo's `links = "sqlite3"` rule). fn maybe_transpile_source( name: ModuleName, source: ModuleCodeString, @@ -154,7 +49,7 @@ fn maybe_transpile_source( let parsed = deno_ast::parse_module(ParseParams { specifier: deno_core::url::Url::parse(&name).unwrap(), - text: source.into(), + text: source.as_str().into(), media_type, capture_tokens: false, scope_analysis: false, @@ -169,7 +64,13 @@ fn maybe_transpile_source( ..Default::default() }, &deno_ast::TranspileModuleOptions::default(), - &deno_ast::EmitOptions::default(), + // The default appends an inline source map comment, which is a + // syntax error where deno_core evaluates a lazy script as an + // expression. + &deno_ast::EmitOptions { + source_map: deno_ast::SourceMapOption::None, + ..Default::default() + }, ) .unwrap_or_else(|e| panic!("snapshot transpile: emit failed for {name}: {e}")) .into_source(); @@ -177,27 +78,66 @@ fn maybe_transpile_source( Ok((transpiled.text.into(), None)) } +/// One `(specifier, source)` table of the extension scripts the snapshot did +/// not evaluate. The snapshot only carries scripts that ran while it was built, +/// so a script first loaded in a live isolate has to come from here. +/// +/// deno_core binary-searches the table and reads each source as ASCII without +/// checking, so it must be sorted by specifier, free of duplicates, and ASCII. +fn residual_table( + name: &str, + files: impl Iterator, + consumed: &HashSet, +) -> String { + let mut files: Vec<_> = files.filter(|f| !consumed.contains(f.specifier)).collect(); + files.sort_by_key(|f| f.specifier); + files.dedup_by_key(|f| f.specifier); + + let mut out = format!("pub static {name}: &[(&str, &str)] = &[\n"); + for file in files { + let source = file + .load() + .unwrap_or_else(|e| panic!("could not load {}: {e}", file.specifier)); + let (code, _) = + maybe_transpile_source(ModuleName::from_static(file.specifier), source).unwrap(); + assert!( + code.as_str().is_ascii(), + "{} is not ASCII after transpiling", + file.specifier + ); + writeln!(out, " ({:?}, {:?}),", file.specifier, code.as_str()).unwrap(); + } + out.push_str("];\n"); + out +} + fn main() { println!("cargo:rustc-env=TARGET={}", env::var("TARGET").unwrap()); println!("cargo:rustc-env=PROFILE={}", env::var("PROFILE").unwrap()); - let exts = vec![ - deno_telemetry::deno_telemetry::init(), - deno_webidl::deno_webidl::init(), - deno_url::deno_url::init(), - deno_console::deno_console::init(), - deno_web::deno_web::init::(Arc::new(BlobStore::default()), None), - deno_crypto::deno_crypto::init(None), - deno_fetch::deno_fetch::init::(Default::default()), - deno_net::deno_net::init::(None, None), + // Same list and order as `create_nativets_runtime`, which supplies their + // state through `lazy_init_extensions`. + let exts: Vec = vec![ + deno_telemetry::deno_telemetry::lazy_init(), + deno_webidl::deno_webidl::lazy_init(), + deno_web::deno_web::lazy_init(), + deno_crypto::deno_crypto::lazy_init(), + deno_fetch::deno_fetch::lazy_init(), + deno_net::deno_net::lazy_init(), fetch::init(), ]; - // Build the file path to the snapshot. + let lazy_js: Vec<_> = exts + .iter() + .flat_map(|e| e.lazy_loaded_js_files.iter().cloned()) + .collect(); + let lazy_esm: Vec<_> = exts + .iter() + .flat_map(|e| e.lazy_loaded_esm_files.iter().cloned()) + .collect(); + let o = PathBuf::from(env::var_os("OUT_DIR").unwrap()); - let snapshot_path = o.join("FETCH_SNAPSHOT.bin"); - // Create the snapshot. let output = deno_core::snapshot::create_snapshot( deno_core::snapshot::CreateSnapshotOptions { cargo_manifest_dir: env!("CARGO_MANIFEST_DIR"), @@ -213,9 +153,17 @@ fn main() { ) .unwrap(); - let mut file = std::fs::File::create(snapshot_path).unwrap(); + let mut file = std::fs::File::create(o.join("FETCH_SNAPSHOT.bin")).unwrap(); file.write_all(&output.output).unwrap(); + let consumed: HashSet = output.consumed_lazy_specifiers.into_iter().collect(); + let residual = format!( + "{}{}", + residual_table("RESIDUAL_LAZY_JS", lazy_js.into_iter(), &consumed), + residual_table("RESIDUAL_LAZY_ESM", lazy_esm.into_iter(), &consumed), + ); + std::fs::write(o.join("residual_lazy_sources.rs"), residual).unwrap(); + for path in output.files_loaded_during_snapshot { println!("cargo:rerun-if-changed={}", path.display()); } diff --git a/backend/windmill-runtime-nativets/src/lib.rs b/backend/windmill-runtime-nativets/src/lib.rs index 75aacbcc64..750cdaa609 100644 --- a/backend/windmill-runtime-nativets/src/lib.rs +++ b/backend/windmill-runtime-nativets/src/lib.rs @@ -22,9 +22,7 @@ mod smoke_tests; mod cert_tests; use std::{ - borrow::Cow, cell::RefCell, - path::PathBuf, rc::Rc, sync::{Arc, LazyLock, Mutex}, }; @@ -32,17 +30,55 @@ use std::{ // Re-export deno_telemetry for use by windmill-worker's otel proxy pub use deno_telemetry; +/// The real environment and filesystem, except that the OTLP protocol always +/// reads as HTTP. +/// +/// The caller points `OTEL_EXPORTER_OTLP_ENDPOINT` at its own HTTP collector +/// only while [`init_telemetry`] runs, and the instance's own exporter shares +/// `OTEL_EXPORTER_OTLP_PROTOCOL`. Honouring `grpc` there would build an exporter +/// that reads the endpoint again on every export, and `console` one that never +/// reads it, so either would send job telemetry past that collector. +struct HttpOtlpSys; + +impl sys_traits::BaseEnvVar for HttpOtlpSys { + fn base_env_var_os(&self, key: &std::ffi::OsStr) -> Option { + if key == "OTEL_EXPORTER_OTLP_PROTOCOL" { + return Some("http/protobuf".into()); + } + sys_traits::impls::RealSys.base_env_var_os(key) + } +} + +impl sys_traits::BaseFsRead for HttpOtlpSys { + fn base_fs_read( + &self, + path: &std::path::Path, + ) -> std::io::Result> { + sys_traits::impls::RealSys.base_fs_read(path) + } +} + +/// `deno_telemetry::init` for the isolates of this process. +pub fn init_telemetry( + rt_config: deno_telemetry::OtelRuntimeConfig, + config: deno_telemetry::OtelConfig, +) -> anyhow::Result<()> { + deno_telemetry::init(&HttpOtlpSys, rt_config, config) +} + use deno_ast::ParseParams; use deno_core::{ + error::CoreErrorKind, op2, serde_v8, url, v8::{self, IsolateHandle}, Extension, JsRuntime, OpState, PollEventLoopOptions, RuntimeOptions, }; use deno_error::JsErrorBox; -use deno_fetch::FetchPermissions; -use deno_net::NetPermissions; +use deno_permissions::{ + Permissions, PermissionsContainer, PermissionsOptions, RuntimePermissionDescriptorParser, +}; use deno_tls::{rustls::pki_types::CertificateDer, rustls::RootCertStore, RootCertStoreProvider}; -use deno_web::{BlobStore, TimersPermission}; +use deno_web::{BlobStore, InMemoryBroadcastChannel}; use itertools::Itertools; use lazy_static::lazy_static; use regex::Regex; @@ -57,156 +93,55 @@ use windmill_common::worker::{write_file, Connection, WINDMILL_DIR}; // ── Snapshot-matched extensions ────────────────────────────────────── // -// `deno_core` 0.352 validates that the snapshot's extension list is a -// *prefix* of the runtime's extension list (snapshot does not need an -// exact match — runtime is allowed to add extensions at the tail, but -// must not reorder or omit any that the snapshot baked in). +// The snapshot (build.rs) is built from the deno_* extensions in their +// `lazy_init()` form followed by this local `fetch` ext. deno_core requires the +// runtime's extension list to start with that same list in that same order; it +// may append more, which is how the windmill `ext` carrying our own ops gets in. // -// Our snapshot (in build.rs) is the same eight deno_* extensions ending -// with this local `fetch` ext. The runtime adds one extra entry at the -// end — the windmill `ext` carrying our own ops — which is fine because -// it's after the snapshot prefix. -// -// This local `fetch` extension declaration must be present in both -// build.rs and lib.rs so the type passes through the `init()` macro. -// The ESM is already in the snapshot, so this `init()` call at runtime -// is a no-op for esm — the registration just records the ext. +// This declaration must exist in both build.rs and lib.rs. The ESM is already +// in the snapshot, so `init()` at runtime only records the extension. deno_core::extension!( fetch, esm_entry_point = "ext:fetch/src/runtime.js", esm = ["src/runtime.js"], ); -// ── Permission container ───────────────────────────────────────────── +include!(concat!(env!("OUT_DIR"), "/residual_lazy_sources.rs")); -/// `no_network` is what `//no_network` promises: no outbound traffic of any kind, -/// the Windmill API included (the client module reaches it through `fetch`). -/// Enforced here and by disabling the deno_net ops, never in JS: user code can -/// call `Deno.core.ops.*` directly and skip any JS-level guard. -pub struct PermissionsContainer { - pub no_network: bool, -} - -const NO_NETWORK_ANNOTATION: &str = "no_network"; +// ── Permissions ────────────────────────────────────────────────────── /// Every op deno_net registers, see `create_nativets_runtime`. Read from the /// extension itself so an upgrade that adds an op cannot leave it enabled. static NO_NETWORK_DISABLED_OPS: LazyLock> = LazyLock::new(|| { - deno_net::deno_net::init::(None, None) + deno_net::deno_net::lazy_init() .ops .iter() .map(|op| op.name) .collect() }); -impl PermissionsContainer { - fn deny_net(&self, target: &str) -> Result<(), deno_permissions::PermissionCheckError> { - if self.no_network { - Err(deno_permissions::PermissionDeniedError::Fatal { - access: format!( - "net access to {target} (the script is annotated //{NO_NETWORK_ANNOTATION})" - ), - } - .into()) - } else { - Ok(()) - } - } -} - -impl FetchPermissions for PermissionsContainer { - #[inline(always)] - fn check_net_url( - &mut self, - url: &deno_core::url::Url, - _api_name: &str, - ) -> Result<(), deno_permissions::PermissionCheckError> { - self.deny_net(&format!("\"{}\"", url.host_str().unwrap_or_default())) - } - - #[inline(always)] - fn check_read<'a>( - &mut self, - path: Cow<'a, std::path::Path>, - _api_name: &str, - _get_path: &'a dyn deno_fs::GetPath, - ) -> Result, deno_io::fs::FsError> { - Ok(deno_fs::CheckedPath::Unresolved(path)) - } - - #[inline(always)] - fn check_write<'a>( - &mut self, - path: Cow<'a, std::path::Path>, - _api_name: &str, - _get_path: &'a dyn deno_fs::GetPath, - ) -> Result, deno_io::fs::FsError> { - Ok(deno_fs::CheckedPath::Unresolved(path)) - } - - #[inline(always)] - fn check_net_vsock( - &mut self, - cid: u32, - port: u32, - _api_name: &str, - ) -> Result<(), deno_permissions::PermissionCheckError> { - self.deny_net(&format!("vsock {cid}:{port}")) - } -} - -impl TimersPermission for PermissionsContainer { - #[inline(always)] - fn allow_hrtime(&mut self) -> bool { - true - } -} - -impl NetPermissions for PermissionsContainer { - fn check_read( - &mut self, - p: &str, - _api_name: &str, - ) -> Result { - self.deny_net(&format!("unix socket \"{p}\""))?; - Ok(PathBuf::from(p)) - } - - fn check_write( - &mut self, - p: &str, - _api_name: &str, - ) -> Result { - self.deny_net(&format!("unix socket \"{p}\""))?; - Ok(PathBuf::from(p)) - } - - fn check_net>( - &mut self, - host: &(T, Option), - _api_name: &str, - ) -> Result<(), deno_permissions::PermissionCheckError> { - self.deny_net(&format!("\"{}\"", host.0.as_ref())) - } - - fn check_write_path<'a>( - &mut self, - p: Cow<'a, std::path::Path>, - _api_name: &str, - ) -> Result, deno_permissions::PermissionCheckError> { - self.deny_net(&format!("unix socket \"{}\"", p.display()))?; - Ok(p) - } - - fn check_vsock( - &mut self, - cid: u32, - port: u32, - _api_name: &str, - ) -> Result<(), deno_permissions::PermissionCheckError> { - self.deny_net(&format!("vsock {cid}:{port}")) - } +/// What an isolate may do: reach the network, unless `no_network`, and nothing +/// else (no filesystem, environment, subprocess or FFI). Nothing prompts. +/// +/// `no_network` is what `//no_network` promises: no outbound traffic of any +/// kind, the Windmill API included (the client module reaches it through +/// `fetch`). Enforced here and by disabling the deno_net ops, never in JS: user +/// code can call `Deno.core.ops.*` directly and skip any JS-level guard. +fn isolate_permissions(no_network: bool) -> anyhow::Result { + let parser = Arc::new(RuntimePermissionDescriptorParser::new( + sys_traits::impls::RealSys, + )); + // An empty list means "all hosts", for a grant and for a denial alike. + let options = PermissionsOptions { + allow_net: (!no_network).then(Vec::new), + deny_net: no_network.then(Vec::new), + prompt: false, + ..Default::default() + }; + let permissions = Permissions::from_options(parser.as_ref(), &options) + .map_err(|e| anyhow::anyhow!("invalid isolate permissions: {e}"))?; + Ok(PermissionsContainer::new(parser, permissions)) } // ── Types ──────────────────────────────────────────────────────────── @@ -228,7 +163,7 @@ pub struct NativeAnnotation { /// script; `None` leaves the default in force. pub fetch_response_timeout_secs: Option, /// `//no_network`: the isolate may open no connection at all, see - /// [`PermissionsContainer`]. + /// [`isolate_permissions`]. pub no_network: bool, } @@ -413,13 +348,10 @@ pub fn setup_deno_runtime() -> anyhow::Result<()> { // deno_fetch requires a TLS provider; install ring as default (idempotent). let _ = rustls::crypto::ring::default_provider().install_default(); - let unrecognized_v8_flags = deno_core::v8_set_flags(vec![ - "--stack-size=1024".to_string(), - "--no-harmony-import-assertions".to_string(), - ]) - .into_iter() - .skip(1) - .collect::>(); + let unrecognized_v8_flags = deno_core::v8_set_flags(vec!["--stack-size=1024".to_string()]) + .into_iter() + .skip(1) + .collect::>(); if !unrecognized_v8_flags.is_empty() { init_err = Some(format!( @@ -435,7 +367,7 @@ pub fn setup_deno_runtime() -> anyhow::Result<()> { // SIGSEGV in WasmCodePointerTable::AllocateUninitializedEntry() on x86_64 Linux. // See: https://github.com/denoland/deno_core/issues/952 let platform = deno_core::v8::new_unprotected_default_platform(0, false).make_shared(); - deno_core::JsRuntime::init_platform(Some(platform), false); + deno_core::JsRuntime::init_platform(Some(platform)); }); if let Some(msg) = init_err { @@ -666,17 +598,15 @@ pub(crate) fn create_nativets_runtime( ..Default::default() }; + // Same list and order as the snapshot (build.rs); their state follows in + // `lazy_init_extensions` below, which must match it too. let exts: Vec = vec![ - deno_telemetry::deno_telemetry::init(), - deno_webidl::deno_webidl::init(), - deno_url::deno_url::init(), - deno_console::deno_console::init(), - deno_web::deno_web::init::(Arc::new(BlobStore::default()), None), - // Registered after deno_web to keep the snapshot (build.rs) a prefix of - // the runtime extension list; deno_crypto declares deps = [deno_webidl, deno_web]. - deno_crypto::deno_crypto::init(None), - deno_fetch::deno_fetch::init::(fetch_options), - deno_net::deno_net::init::(None, None), + deno_telemetry::deno_telemetry::lazy_init(), + deno_webidl::deno_webidl::lazy_init(), + deno_web::deno_web::lazy_init(), + deno_crypto::deno_crypto::lazy_init(), + deno_fetch::deno_fetch::lazy_init(), + deno_net::deno_net::lazy_init(), fetch::init(), ext, ]; @@ -688,6 +618,8 @@ pub(crate) fn create_nativets_runtime( deno_core::v8::CreateParams::default().heap_limits(0, 1024 * 1024 * 128), ), startup_snapshot: Some(RUNTIME_SNAPSHOT), + residual_lazy_js_sources: RESIDUAL_LAZY_JS, + residual_lazy_esm_sources: RESIDUAL_LAZY_ESM, module_loader: Some(Rc::new(deno_core::FsModuleLoader)), extension_transpiler: None, ..Default::default() @@ -714,12 +646,28 @@ pub(crate) fn create_nativets_runtime( y * 2 }); + js_runtime + .lazy_init_extensions(vec![ + deno_telemetry::deno_telemetry::args(), + deno_webidl::deno_webidl::args(), + deno_web::deno_web::args( + Arc::new(BlobStore::default()), + None, + false, + InMemoryBroadcastChannel::default(), + ), + deno_crypto::deno_crypto::args(None), + deno_fetch::deno_fetch::args(fetch_options), + deno_net::deno_net::args(None, None), + ]) + .map_err(windmill_common::error::to_anyhow)?; + let (log_sender, log_receiver) = mpsc::unbounded_channel::(); { let op_state = js_runtime.op_state(); let mut op_state = op_state.borrow_mut(); - op_state.put(PermissionsContainer { no_network }); + op_state.put(isolate_permissions(no_network)?); op_state.put(MainArgs { args: initial_args }); op_state.put(LogString { s: log_sender }); } @@ -731,7 +679,7 @@ pub(crate) fn create_nativets_runtime( .execute_script( "", format!( - "globalThis.__wmInitPerIsolate({{ fetchResponseTimeoutMs: {fetch_response_timeout_ms} }})" + "globalThis.__wmInitPerIsolate({{ fetchResponseTimeoutMs: {fetch_response_timeout_ms}, noNetwork: {no_network} }})" ), ) .map_err(windmill_common::error::to_anyhow)?; @@ -777,7 +725,7 @@ pub(crate) fn extract_global_string( js_runtime: &mut JsRuntime, global: v8::Global, ) -> Result, String> { - let scope = &mut js_runtime.handle_scope(); + deno_core::scope!(scope, js_runtime); let local = v8::Local::new(scope, global); match serde_v8::from_v8::>(scope, local) { Ok(s) => Ok(unsafe_raw(s.unwrap_or_else(|| "null".to_string()))), @@ -1169,7 +1117,10 @@ import("file:///eval.ts").then((module) => module.{main_fn}(...args)) Ok(global) => { extract_global_string(js_runtime, global).map_err(|e| ExecuteError::Script(e)) } - Err(deno_core::error::CoreError::Js(e)) => { + Err(e) if matches!(e.as_kind(), CoreErrorKind::Js(_)) => { + let CoreErrorKind::Js(e) = e.into_kind() else { + unreachable!("matched as a JS error above") + }; let source = e.frames.first().and_then(|f| { f.file_name .as_ref() diff --git a/backend/windmill-runtime-nativets/src/runtime.js b/backend/windmill-runtime-nativets/src/runtime.js index 88e10f765a..86d20dd022 100644 --- a/backend/windmill-runtime-nativets/src/runtime.js +++ b/backend/windmill-runtime-nativets/src/runtime.js @@ -1,39 +1,42 @@ -import * as abortSignal from "ext:deno_web/03_abort_signal.js"; -import * as domException from "ext:deno_web/01_dom_exception.js"; -import * as base64 from "ext:deno_web/05_base64.js"; -import * as console from "ext:deno_console/01_console.js"; -import * as encoding from "ext:deno_web/08_text_encoding.js"; -import * as event from "ext:deno_web/02_event.js"; -import * as fetch from "ext:deno_fetch/26_fetch.js"; -import * as file from "ext:deno_web/09_file.js"; -import * as fileReader from "ext:deno_web/10_filereader.js"; -import * as formData from "ext:deno_fetch/21_formdata.js"; -import * as headers from "ext:deno_fetch/20_headers.js"; -import * as streams from "ext:deno_web/06_streams.js"; -import * as timers from "ext:deno_web/02_timers.js"; -import * as url from "ext:deno_url/00_url.js"; -import * as net from "ext:deno_net/01_net.js"; -import * as tls from "ext:deno_net/02_tls.js"; -import * as urlPattern from "ext:deno_url/01_urlpattern.js"; -import * as webidl from "ext:deno_webidl/00_webidl.js"; -import * as crypto from "ext:deno_crypto/00_crypto.js"; -import * as response from "ext:deno_fetch/23_response.js"; -import * as request from "ext:deno_fetch/23_request.js"; -import "ext:deno_web/02_structured_clone.js"; -import * as globalInterfaces from "ext:deno_web/04_global_interfaces.js"; -// Namespace imports (not side-effect-only) so their constructors are reachable -// for the globalThis wiring below. The module bodies still execute on -// evaluation, so their side effects apply. -import * as messagePort from "ext:deno_web/13_message_port.js"; -import * as compression from "ext:deno_web/14_compression.js"; -import * as performance from "ext:deno_web/15_performance.js"; -import "ext:deno_web/16_image_data.js"; -import "ext:deno_fetch/27_eventsource.js"; +import { core, internals } from "ext:core/mod.js"; + +// deno_crypto brands CryptoKeys with this symbol and reads it from `internals` +// when it evaluates; in Deno a deno_node module sets it, which is not loaded here. +internals.kKeyObject ??= Symbol("kKeyObject"); + +// Every script loaded here runs at snapshot-build time and is baked into the +// snapshot. One that is first loaded at run time comes from the residual table +// build.rs emits instead. +const abortSignal = core.loadExtScript("ext:deno_web/03_abort_signal.js"); +const domException = core.loadExtScript("ext:deno_web/01_dom_exception.js"); +const base64 = core.loadExtScript("ext:deno_web/05_base64.js"); +const console = core.loadExtScript("ext:deno_web/01_console.js"); +const encoding = core.loadExtScript("ext:deno_web/08_text_encoding.js"); +const event = core.loadExtScript("ext:deno_web/02_event.js"); +const fetch = core.loadExtScript("ext:deno_fetch/26_fetch.js"); +const file = core.loadExtScript("ext:deno_web/09_file.js"); +const fileReader = core.loadExtScript("ext:deno_web/10_filereader.js"); +const formData = core.loadExtScript("ext:deno_fetch/21_formdata.js"); +const headers = core.loadExtScript("ext:deno_fetch/20_headers.js"); +const streams = core.loadExtScript("ext:deno_web/06_streams.js"); +const timers = core.loadExtScript("ext:deno_web/02_timers.js"); +const url = core.loadExtScript("ext:deno_web/00_url.js"); +const urlPattern = core.loadExtScript("ext:deno_web/01_urlpattern.js"); +const webidl = core.loadExtScript("ext:deno_webidl/00_webidl.js"); +const crypto = core.loadExtScript("ext:deno_crypto/00_crypto.js"); +const response = core.loadExtScript("ext:deno_fetch/23_response.js"); +const request = core.loadExtScript("ext:deno_fetch/23_request.js"); +const globalInterfaces = core.loadExtScript( + "ext:deno_web/04_global_interfaces.js", +); +const messagePort = core.loadExtScript("ext:deno_web/13_message_port.js"); +const compression = core.loadExtScript("ext:deno_web/14_compression.js"); +const performance = core.loadExtScript("ext:deno_web/15_performance.js"); // deno_fetch applies no deadline, so a peer that accepts a request and then // never answers leaves `await fetch(...)` pending until the job timeout, which // self-hosted defaults to 7 days. -const ORIGINAL_FETCH = fetch.fetch; +const DENO_FETCH = fetch.fetch; // deno_web's timers reject any `this` other than undefined/globalThis, so // `timers.setTimeout(...)` passes the module namespace and throws "Illegal @@ -50,6 +53,14 @@ const abortControllerAbort = Function.prototype.call.bind( ); const ReflectApply = Reflect.apply; +// Forwards the original argument count, like every caller below relies on. The +// extra promise hop under `no_network` costs the same-tick settling of an +// aborted fetch, which only matters where a request could have been made. +function ORIGINAL_FETCH() { + const p = ReflectApply(DENO_FETCH, undefined, arguments); + return noNetwork ? promiseThen(p, undefined, rewordNoNetworkDenial) : p; +} + // Installed per isolate by __wmInitPerIsolate; 0 disables. Only a backstop // for the impossible case of fetch running before that init. let fetchResponseTimeoutMs = 900_000; @@ -74,6 +85,21 @@ function fetchResponseTimeoutError(requestUrl, timeoutMs) { ); } +// Set per isolate for a `//no_network` script. The denial itself is the +// isolate's permissions (Rust); this only replaces deno's "run again with the +// --allow-net flag" hint, which names a flag a script author cannot pass. +let noNetwork = false; + +function rewordNoNetworkDenial(e) { + if (e?.name === "NotCapable" && typeof e.message === "string") { + e.message = e.message.replace( + ", run again with the --allow-net flag", + ": the script is annotated //no_network", + ); + } + throw e; +} + globalThis.atob = base64.atob; globalThis.btoa = base64.btoa; // Not `async`, for the same reason deno_fetch's own outer fetch isn't: WPT @@ -152,7 +178,22 @@ globalThis.console = new console.Console((msg, level) => ); globalThis.AbortController = abortSignal.AbortController; globalThis.AbortSignal = abortSignal.AbortSignal; -globalThis.crypto = crypto.crypto; +// A getter, not a value: `crypto.crypto` mints a cppgc object, and the cppgc +// heap is not attached while this module runs at snapshot-build time. +Object.defineProperty(globalThis, "crypto", { + configurable: true, + enumerable: true, + get: () => crypto.crypto, + // Assignment has to keep working, as it does on a plain property. + set(value) { + Object.defineProperty(globalThis, "crypto", { + configurable: true, + enumerable: true, + writable: true, + value, + }); + }, +}); globalThis.Crypto = crypto.Crypto; globalThis.CryptoKey = crypto.CryptoKey; globalThis.SubtleCrypto = crypto.SubtleCrypto; @@ -233,6 +274,7 @@ globalThis.__wmInitPerIsolate = (config) => { if (config != null && typeof config.fetchResponseTimeoutMs === "number") { fetchResponseTimeoutMs = config.fetchResponseTimeoutMs; } + noNetwork = config?.noNetwork === true; // setTimeOrigin() seeds performance.timeOrigin from the isolate's wall clock; // without it timeOrigin is undefined and `timeOrigin + performance.now()` is NaN. @@ -256,13 +298,14 @@ globalThis.__wmInitPerIsolate = (config) => { }; // Expose bootstrapOtel globally so it can be called from Rust after runtime creation. -// We use dynamic import so deno_telemetry isn't loaded during snapshot creation. +// Loaded on first call so deno_telemetry isn't evaluated during snapshot creation. // Config: [tracingEnabled, metricsEnabled, consoleConfig, deterministic] // consoleConfig: 0=ignore, 1=capture, 2=replace globalThis.__bootstrapOtel = () => { - import("ext:deno_telemetry/telemetry.ts").then(({ bootstrap, enterSpan }) => { - bootstrap([1, 0, 1, 0]); - // Expose enterSpan for setting parent trace context - globalThis.__enterSpan = enterSpan; - }); + const { bootstrap, enterSpan } = core.loadExtScript( + "ext:deno_telemetry/telemetry.ts", + ); + bootstrap([1, 0, 1, 0]); + // Expose enterSpan for setting parent trace context + globalThis.__enterSpan = enterSpan; }; diff --git a/backend/windmill-runtime-nativets/src/smoke_tests.rs b/backend/windmill-runtime-nativets/src/smoke_tests.rs index f3da460227..1373a8abd4 100644 --- a/backend/windmill-runtime-nativets/src/smoke_tests.rs +++ b/backend/windmill-runtime-nativets/src/smoke_tests.rs @@ -451,6 +451,10 @@ export async function main(): Promise<{ uuid: string; nonzero: boolean; sha256: .map((b) => b.toString(16).padStart(2, "0")) .join(""); + // `crypto` is installed as an accessor; polyfill-style assignment must + // still work rather than throw for want of a setter. + (globalThis as any).crypto = globalThis.crypto; + return { uuid, nonzero, sha256 }; } "#; diff --git a/backend/windmill-runtime-nativets/tests/no_network.rs b/backend/windmill-runtime-nativets/tests/no_network.rs index 876207fc26..dc7086f22e 100644 --- a/backend/windmill-runtime-nativets/tests/no_network.rs +++ b/backend/windmill-runtime-nativets/tests/no_network.rs @@ -55,6 +55,9 @@ async function attempt(f) {{ export async function main() {{ return {{ fetch: await attempt(() => fetch("http://127.0.0.1:{port}/").then((r) => r.text())), + data_url: await attempt(() => fetch("data:text/plain,ok").then((r) => r.text())), + blob_url: await attempt(() => fetch(URL.createObjectURL(new Blob(["ok"]))).then((r) => r.text())), + raw_fetch: await attempt(() => Deno.core.ops.op_fetch("GET", "http://127.0.0.1:{port}/", [], null, false, null, null)), tcp: await attempt(() => Deno.core.ops.op_net_connect_tcp({{ hostname: "127.0.0.1", port: {port} }}, null, null)), unix: await attempt(() => Deno.core.ops.op_net_connect_unix("/var/run/docker.sock")), quic: await attempt(() => Deno.core.ops.op_quic_endpoint_create({{ hostname: "localhost", port: 0 }}, true)), @@ -76,6 +79,15 @@ async fn no_network_denies_fetch_and_raw_socket_ops() { fetch.contains("no_network"), "fetch was not denied: {fetch}" ); + // Neither opens a connection, so neither is network access. + assert_eq!(out["data_url"], "allowed", "{out}"); + assert_eq!(out["blob_url"], "allowed", "{out}"); + // The global `fetch` only words the refusal; the op under it is what refuses. + let raw_fetch = out["raw_fetch"].as_str().unwrap(); + assert!( + raw_fetch.contains("Requires net access"), + "the fetch op was not denied: {raw_fetch}" + ); for key in ["tcp", "unix", "quic"] { let msg = out[key].as_str().unwrap(); assert!( diff --git a/backend/windmill-runtime-nativets/tests/otel_e2e.rs b/backend/windmill-runtime-nativets/tests/otel_e2e.rs index 22fd18de90..d1853c461d 100644 --- a/backend/windmill-runtime-nativets/tests/otel_e2e.rs +++ b/backend/windmill-runtime-nativets/tests/otel_e2e.rs @@ -71,7 +71,7 @@ async fn spawn_capturing_http(captured: Arc>>>) -> u16 { /// drifts away from `tracing_enabled + Capture`, this test breaks /// before the customer-facing panic does. fn init_with_ee_otel_config() { - deno_telemetry::init( + windmill_runtime_nativets::init_telemetry( deno_telemetry::OtelRuntimeConfig { runtime_name: "windmill-nativets".into(), runtime_version: "0".into(), @@ -157,13 +157,12 @@ export async function main(): Promise {{ "fetch target should have been hit exactly once" ); - // 4. Force the BatchSpanProcessor to flush so the exporter posts - // to our mock collector synchronously (default flush interval - // is ~5s; tests can't wait that long). - deno_telemetry::flush(); - // Exporter is async over the OTel runtime; give it a beat to - // actually send the HTTP request. - tokio::time::sleep(Duration::from_millis(500)).await; + // 4. The BatchSpanProcessor exports on its own schedule (about 5s by + // default) and deno_telemetry exposes no flush, so wait for it. + let deadline = tokio::time::Instant::now() + Duration::from_secs(20); + while otlp_hits.lock().await.is_empty() && tokio::time::Instant::now() < deadline { + tokio::time::sleep(Duration::from_millis(100)).await; + } let otlp_captured = otlp_hits.lock().await; assert!( diff --git a/backend/windmill-runtime-nativets/tests/otel_init.rs b/backend/windmill-runtime-nativets/tests/otel_init.rs index fdf1ac944d..80ef8683aa 100644 --- a/backend/windmill-runtime-nativets/tests/otel_init.rs +++ b/backend/windmill-runtime-nativets/tests/otel_init.rs @@ -46,7 +46,7 @@ async fn ee_otel_init_config_populates_otel_globals() { // Step 1: reproduce the footgun. `OtelConfig::default()` has // tracing/metrics off and `console = Ignore`, which trips the // 0.31 early-return — Ok(()) but OnceCell stays empty. - deno_telemetry::init( + windmill_runtime_nativets::init_telemetry( deno_telemetry::OtelRuntimeConfig { runtime_name: "windmill-nativets-test".into(), runtime_version: "0".into(), @@ -63,10 +63,17 @@ async fn ee_otel_init_config_populates_otel_globals() { tracing_enabled/console fields." ); + // The instance's own exporter shares this variable and may be set to gRPC, + // or to anything else. Native telemetry must not read it: a value + // deno_telemetry rejects outright shows it is never consulted. + // SAFETY: the only test in this binary, so nothing reads the environment + // concurrently. + unsafe { std::env::set_var("OTEL_EXPORTER_OTLP_PROTOCOL", "not-a-protocol") }; + // Step 2: re-call init with the exact config shape that the EE // `load_internal_otel_exporter` ships — this is what production // hits, so the test exercises the actual prod call path. - deno_telemetry::init( + windmill_runtime_nativets::init_telemetry( deno_telemetry::OtelRuntimeConfig { runtime_name: "windmill-nativets".into(), runtime_version: "0".into(), diff --git a/flake.nix b/flake.nix index f005644e3d..2a1d2699c8 100644 --- a/flake.nix +++ b/flake.nix @@ -78,15 +78,16 @@ ]; # --------------------------------------------------------------- - # Prebuilt V8 binary (must match version in Cargo.toml) + # Prebuilt V8 binary (must match the `v8` pin in backend/Cargo.toml). + # The simdutf variant: deno_core enables that feature of the v8 crate. # --------------------------------------------------------------- rustyV8Archive = let - version = "137.1.0"; + version = "150.4.0"; target = stdenv.hostPlatform.rust.rustcTarget; sha256 = { x86_64-linux = - "sha256-Tiscfy2bzYGR3s0T+SC1IB3xWvTVpVcSEdjq3MCRoRw="; + "sha256-9IdiyhDR8fxgWkQcWuQw7Izh6egPFNePvELLh4wwtHY="; aarch64-linux = lib.fakeHash; x86_64-darwin = lib.fakeHash; aarch64-darwin = lib.fakeHash; @@ -94,7 +95,7 @@ in pkgs.fetchurl { name = "librusty_v8-${version}"; url = - "https://github.com/denoland/rusty_v8/releases/download/v${version}/librusty_v8_release_${target}.a.gz"; + "https://github.com/denoland/rusty_v8/releases/download/v${version}/librusty_v8_simdutf_release_${target}.a.gz"; inherit sha256; }; From dedd77587f3234f3e480388b00340394794ac5bb Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sat, 10 Oct 2026 12:40:00 +0200 Subject: [PATCH 10/20] fix: keep preserve_step_tags on saved branch and loop flow nodes (#11660) * fix: keep preserve_step_tags on saved branch and loop flow nodes Co-Authored-By: Claude Opus 5.5 * style: format reduce_flow call Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- backend/tests/worker.rs | 95 +++++++++++++++++++ .../windmill-worker/src/worker_lockfiles.rs | 9 ++ 2 files changed, 104 insertions(+) diff --git a/backend/tests/worker.rs b/backend/tests/worker.rs index eebb14e07e..751752eb00 100644 --- a/backend/tests/worker.rs +++ b/backend/tests/worker.rs @@ -5000,6 +5000,101 @@ async fn test_flow_lock_all(db: Pool) -> anyhow::Result<()> { Ok(()) } +/// A deployed flow runs a multi-step loop body as the flow node its deploy stored, which +/// carries its own copy of the flow's `preserve_step_tags`. A preview never loads that node. +#[sqlx::test(fixtures("base"))] +async fn test_saved_flow_preserves_nested_step_tags(db: Pool) -> anyhow::Result<()> { + use futures::StreamExt; + + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + let client = reqwest::Client::new(); + let flow = json!({ + "path": "g/all/saved_step_tags", "summary": "", "description": "", "tag": "python3", + "schema": { "type": "object", "properties": {}, "required": [] }, + "value": { + "preserve_step_tags": true, + "modules": [{ + "id": "loop", + "value": { + "type": "forloopflow", "skip_failures": false, + "iterator": { "type": "static", "value": [2, 3] }, + "modules": [{ + "id": "own_tag", + "value": { + "type": "rawscript", "language": "bun", "lock": "", "tag": "bun", + "content": "export function main(n: number) { return n * n; }", + "input_transforms": { + "n": { "type": "javascript", "expr": "flow_input.iter.value" } + } + } + }, { + "id": "untagged", + "value": { + "type": "rawscript", "language": "bun", "lock": "", + "content": "export function main(n: number) { return n + 1; }", + "input_transforms": { + "n": { "type": "javascript", "expr": "results.own_tag" } + } + } + }] + } + }] + } + }); + let mut completed = listen_for_completed_jobs(&db).await; + client + .post(format!( + "http://localhost:{port}/api/w/test-workspace/flows/create" + )) + .bearer_auth("SECRET_TOKEN") + .json(&flow) + .send() + .await? + .error_for_status()?; + let deployment = in_test_worker(&db, completed.next(), port).await.unwrap(); + let deployed = completed_job(deployment, &db).await; + assert!(deployed.success, "{:?}", deployed.result); + + let job_id = client + .post(format!( + "http://localhost:{port}/api/w/test-workspace/jobs/run/f/g/all/saved_step_tags" + )) + .bearer_auth("SECRET_TOKEN") + .json(&json!({})) + .send() + .await? + .error_for_status()? + .text() + .await? + .parse::()?; + in_test_worker(&db, completed.find(&job_id), port).await; + let job = completed_job(job_id, &db).await; + assert!(job.success, "{:?}", job.result); + let tags = sqlx::query_as::<_, (String, String)>( + "WITH RECURSIVE descendants AS ( + SELECT id FROM v2_job WHERE id = $1 + UNION ALL + SELECT child.id FROM v2_job child JOIN descendants ON child.parent_job = descendants.id + ) + SELECT DISTINCT flow_step_id, tag FROM v2_job + WHERE id IN (SELECT id FROM descendants) AND flow_step_id IN ('own_tag', 'untagged') + ORDER BY flow_step_id, tag", + ) + .bind(job_id) + .fetch_all(&db) + .await?; + assert_eq!( + tags, + vec![ + ("own_tag".to_owned(), "bun".to_owned()), + ("untagged".to_owned(), "python3".to_owned()) + ] + ); + Ok(()) +} + #[cfg(feature = "deno_core")] #[sqlx::test(fixtures("base"))] diff --git a/backend/windmill-worker/src/worker_lockfiles.rs b/backend/windmill-worker/src/worker_lockfiles.rs index b608601991..6189d39b8d 100644 --- a/backend/windmill-worker/src/worker_lockfiles.rs +++ b/backend/windmill-worker/src/worker_lockfiles.rs @@ -1244,6 +1244,7 @@ pub async fn handle_flow_dependency_job( &job.workspace_id, flow.failure_module.as_ref(), flow.same_worker, + flow.preserve_step_tags, ) .await?; @@ -2134,6 +2135,7 @@ async fn insert_flow_modules<'c>( workspace_id: &str, failure_module: Option<&Box>, same_worker: bool, + preserve_step_tags: bool, modules: &mut Vec, modules_node: &mut Option, ) -> Result> { @@ -2144,6 +2146,7 @@ async fn insert_flow_modules<'c>( workspace_id, failure_module, same_worker, + preserve_step_tags, )) .await?; if modules.is_empty() || crate::worker_flow::is_simple_modules(modules, failure_module) { @@ -2160,6 +2163,7 @@ async fn insert_flow_modules<'c>( modules: std::mem::take(modules), failure_module: failure_module.cloned(), same_worker, + preserve_step_tags, ..Default::default() }))), None, @@ -2176,6 +2180,7 @@ async fn reduce_flow<'c>( workspace_id: &str, failure_module: Option<&Box>, same_worker: bool, + preserve_step_tags: bool, ) -> Result> { use FlowModuleValue::*; for module in &mut *modules { @@ -2234,6 +2239,7 @@ async fn reduce_flow<'c>( workspace_id, failure_module, same_worker, + preserve_step_tags, modules, modules_node, ) @@ -2247,6 +2253,7 @@ async fn reduce_flow<'c>( workspace_id, failure_module, same_worker, + preserve_step_tags, &mut branch.modules, &mut branch.modules_node, ) @@ -2258,6 +2265,7 @@ async fn reduce_flow<'c>( workspace_id, failure_module, same_worker, + preserve_step_tags, default, default_node, ) @@ -2271,6 +2279,7 @@ async fn reduce_flow<'c>( workspace_id, failure_module, same_worker, + preserve_step_tags, &mut branch.modules, &mut branch.modules_node, ) From 74c9b0cb2eebc16dd5e23f23be89b127bcbe6ec2 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sat, 10 Oct 2026 12:45:20 +0200 Subject: [PATCH 11/20] feat: add python 3.14 and 3.15 to the supported instance versions (#11654) Co-authored-by: Claude Opus 5.5 --- .../windmill-parser-py-imports/src/lib.rs | 11 ++++++++--- backend/src/main.rs | 5 ++++- backend/windmill-common/src/worker.rs | 17 +++++++++++++++-- backend/windmill-worker/src/worker.rs | 2 ++ cli/src/utils/script_common.ts | 4 +++- frontend/src/lib/components/instanceSettings.ts | 8 +++++++- 6 files changed, 39 insertions(+), 8 deletions(-) diff --git a/backend/parsers/windmill-parser-py-imports/src/lib.rs b/backend/parsers/windmill-parser-py-imports/src/lib.rs index de7f51bc79..72597da385 100644 --- a/backend/parsers/windmill-parser-py-imports/src/lib.rs +++ b/backend/parsers/windmill-parser-py-imports/src/lib.rs @@ -351,13 +351,16 @@ async fn parse_python_imports_inner( temp_script_refs: &Option>, ) -> error::Result> { tracing::debug!("Parsing python imports for path: {}", path); - let PythonAnnotations { py310, py311, py312, py313, .. } = PythonAnnotations::parse(&code); + let PythonAnnotations { py310, py311, py312, py313, py314, py315, .. } = + PythonAnnotations::parse(&code); tracing::debug!( - "Found python annotations - py310: {}, py311: {}, py312: {}, py313: {}", + "Found python annotations - py310: {}, py311: {}, py312: {}, py313: {}, py314: {}, py315: {}", py310, py311, py312, - py313 + py313, + py314, + py315 ); let mut push_version_specifiers = |perform, unparsed: String| -> error::Result<()> { @@ -373,6 +376,8 @@ async fn parse_python_imports_inner( push_version_specifiers(py311, "==3.11.*".to_owned())?; push_version_specifiers(py312, "==3.12.*".to_owned())?; push_version_specifiers(py313, "==3.13.*".to_owned())?; + push_version_specifiers(py314, "==3.14.*".to_owned())?; + push_version_specifiers(py315, "==3.15.*".to_owned())?; for x in code.lines() { if x.starts_with("# py:") || x.starts_with("#py:") { diff --git a/backend/src/main.rs b/backend/src/main.rs index 66ddce60e2..b8fdd9353f 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -123,7 +123,8 @@ use windmill_worker::{ BUN_BUNDLE_CACHE_DIR, BUN_CACHE_DIR, CSHARP_CACHE_DIR, DENO_CACHE_DIR, DENO_CACHE_DIR_DEPS, DENO_CACHE_DIR_NPM, GO_BIN_CACHE_DIR, GO_CACHE_DIR, JAVA_CACHE_DIR, NU_CACHE_DIR, POWERSHELL_CACHE_DIR, PY310_CACHE_DIR, PY311_CACHE_DIR, PY312_CACHE_DIR, PY313_CACHE_DIR, - RUBY_CACHE_DIR, RUST_CACHE_DIR, R_CACHE_DIR, TAR_JAVA_CACHE_DIR, UV_CACHE_DIR, + PY314_CACHE_DIR, PY315_CACHE_DIR, RUBY_CACHE_DIR, RUST_CACHE_DIR, R_CACHE_DIR, + TAR_JAVA_CACHE_DIR, UV_CACHE_DIR, }; use crate::monitor::{ @@ -2179,6 +2180,8 @@ pub async fn run_workers( &*PY311_CACHE_DIR, &*PY312_CACHE_DIR, &*PY313_CACHE_DIR, + &*PY314_CACHE_DIR, + &*PY315_CACHE_DIR, &*BUN_BUNDLE_CACHE_DIR, &*GO_CACHE_DIR, &*GO_BIN_CACHE_DIR, diff --git a/backend/windmill-common/src/worker.rs b/backend/windmill-common/src/worker.rs index 9bc788d9f3..58223c5d4a 100644 --- a/backend/windmill-common/src/worker.rs +++ b/backend/windmill-common/src/worker.rs @@ -1201,6 +1201,8 @@ pub struct PythonAnnotations { pub py311: bool, pub py312: bool, pub py313: bool, + pub py314: bool, + pub py315: bool, pub sandbox: bool, } @@ -2892,6 +2894,8 @@ pub enum PyVAlias { #[default] Py312, Py313, + Py314, + Py315, } impl Into for PyVAlias { @@ -2909,13 +2913,20 @@ impl Into for PyVAlias { impl PyVAlias { pub fn all>() -> Vec { use PyVAlias::*; - vec![Py310.into(), Py311.into(), Py312.into(), Py313.into()] + vec![ + Py310.into(), + Py311.into(), + Py312.into(), + Py313.into(), + Py314.into(), + Py315.into(), + ] } // Get MAJOR part of alias. (semver: MAJOR.MINOR.PATCH) fn major(&self) -> u32 { use PyVAlias::*; match self { - Py310 | Py311 | Py312 | Py313 => 3, + Py310 | Py311 | Py312 | Py313 | Py314 | Py315 => 3, // Py400 | Py401 => 4 } } @@ -2930,6 +2941,8 @@ impl PyVAlias { "311" => Some(Py311), "312" => Some(Py312), "313" => Some(Py313), + "314" => Some(Py314), + "315" => Some(Py315), _ => None, } } diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index 5ae626f27e..7c3c3971fa 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -215,6 +215,8 @@ lazy_static::lazy_static! { pub static ref PY311_CACHE_DIR: String = format!("{}python_3_11", *ROOT_CACHE_DIR); pub static ref PY312_CACHE_DIR: String = format!("{}python_3_12", *ROOT_CACHE_DIR); pub static ref PY313_CACHE_DIR: String = format!("{}python_3_13", *ROOT_CACHE_DIR); + pub static ref PY314_CACHE_DIR: String = format!("{}python_3_14", *ROOT_CACHE_DIR); + pub static ref PY315_CACHE_DIR: String = format!("{}python_3_15", *ROOT_CACHE_DIR); pub static ref TAR_JAVA_CACHE_DIR: String = format!("{}tar/java", *ROOT_CACHE_DIR); diff --git a/cli/src/utils/script_common.ts b/cli/src/utils/script_common.ts index 611268c27c..e3deabb6a7 100644 --- a/cli/src/utils/script_common.ts +++ b/cli/src/utils/script_common.ts @@ -189,6 +189,8 @@ const LANG_ANNOTATIONS: Partial> = { "py311", "py312", "py313", + "py314", + "py315", "sandbox", ], bun: ["npm", "nodejs", "native", "nobundling", "sandbox"], @@ -208,7 +210,7 @@ const LANG_ANNOTATIONS: Partial> = { * * `# py: ` is the exception the macro does not cover: the python * import parser reads it directly (`windmill-parser-py-imports`, alongside the - * `py310`..`py313` flags) to pick the interpreter, which changes what resolves. + * `py310`..`py315` flags) to pick the interpreter, which changes what resolves. */ export function hasLockAffectingAnnotation( scriptContent: string, diff --git a/frontend/src/lib/components/instanceSettings.ts b/frontend/src/lib/components/instanceSettings.ts index 867570e1a1..83fba2b8ac 100644 --- a/frontend/src/lib/components/instanceSettings.ts +++ b/frontend/src/lib/components/instanceSettings.ts @@ -840,7 +840,7 @@ export const settings: Record = { // 1. Change placeholder in instanceSettings.ts // 2. Change LATEST_STABLE_PY in dockerfile // 3. Change #[default] annotation for PyVersion in backend - placeholder: '3.10,3.11,3.12,3.13', + placeholder: '3.10,3.11,3.12,3.13,3.14,3.15', select_items: [ { label: 'Latest Stable', @@ -858,6 +858,12 @@ export const settings: Record = { }, { label: '3.13' + }, + { + label: '3.14' + }, + { + label: '3.15' } ], storage: 'setting' From 0250a90849fc01589e8544b91c680277570a68b6 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sat, 10 Oct 2026 12:50:56 +0200 Subject: [PATCH 12/20] chore: bump tokio to 1.53 lts (#11663) * chore: bump tokio to 1.51 lts Co-Authored-By: Claude Opus 5.5 * chore: move tokio pin to 1.53 lts Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- backend/AGENTS.md | 2 +- backend/Cargo.lock | 73 +++++----------------------------------------- backend/Cargo.toml | 2 +- 3 files changed, 9 insertions(+), 68 deletions(-) diff --git a/backend/AGENTS.md b/backend/AGENTS.md index 750e0594e1..fbacb857ee 100644 --- a/backend/AGENTS.md +++ b/backend/AGENTS.md @@ -214,7 +214,7 @@ Whole classes of defect compile and unit-test clean: - **Stack overflow from a large buffer in an async block.** An array declared across an `.await` is baked into the future's state; once that future is boxed a few layers deep by the job poller, two 16 KB arrays abort the worker *process* (`thread - 'tokio-runtime-worker' has overflowed its stack`). Heap-allocate read buffers + 'tokio-rt-worker' has overflowed its stack`). Heap-allocate read buffers (`vec![0u8; N]`, not `[0u8; N]`). - Deadlocks from draining only one of a child's pipes, missed cancellation or timeout propagation, and anything depending on the real engine's output format. diff --git a/backend/Cargo.lock b/backend/Cargo.lock index efd46867fb..ce517a7487 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -247,7 +238,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6" dependencies = [ - "object 0.39.1", + "object", ] [[package]] @@ -1623,21 +1614,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide 0.8.9", - "object 0.37.3", - "rustc-demangle", - "windows-link 0.2.1", -] - [[package]] name = "base16ct" version = "0.2.0" @@ -5921,12 +5897,6 @@ dependencies = [ "polyval", ] -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - [[package]] name = "git-version" version = "0.3.9" @@ -7055,17 +7025,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "io-uring" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3bd0ecfbb87805f538bb7b32e5239ca0763890c623e349860ecba69469f2bb" -dependencies = [ - "bitflags 2.13.2", - "cfg-if", - "libc", -] - [[package]] name = "ipconfig" version = "0.3.4" @@ -8803,15 +8762,6 @@ dependencies = [ "url", ] -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - [[package]] name = "object" version = "0.39.1" @@ -11201,12 +11151,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "rustc-demangle" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" - [[package]] name = "rustc-hash" version = "1.1.0" @@ -13934,23 +13878,20 @@ dependencies = [ [[package]] name = "tokio" -version = "1.47.1" +version = "1.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", "socket2 0.6.5", "tokio-macros", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -13979,13 +13920,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index e91ad01795..306a3ae9f6 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -451,7 +451,7 @@ headers = "^0" hyper = { version = "^1", features = ["full"] } hyper-tls = "^0.6" hyper-util = { version = "^0.1", features = ["client-legacy", "http1", "tokio"] } -tokio = { version = "=1.47.1", features = ["full", "tracing", "time"] } +tokio = { version = "=1.53.2", features = ["full", "tracing", "time"] } tokio-stream = { version = "0.1.17" } tower = "^0" tower-http = { version = "^0.6", features = ["trace", "cors", "catch-panic"] } From ed7a859e8c4799bed086baf707b817a68f93d2a4 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sat, 10 Oct 2026 13:06:58 +0200 Subject: [PATCH 13/20] chore: bump uv to 0.13.0 (#11665) * chore: bump uv to 0.13.0 Co-Authored-By: Claude Opus 5.5 * fix: pin the python ansible is installed on in the full images Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- .github/DockerfileBackendTests | 2 +- .github/workflows/backend-test-windows.yml | 2 +- .github/workflows/backend-test.yml | 2 +- Dockerfile | 2 +- docker/DockerfileFull | 4 +++- docker/DockerfileFullEe | 4 +++- docker/DockerfileSlim | 2 +- docker/DockerfileSlimEe | 2 +- 8 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/DockerfileBackendTests b/.github/DockerfileBackendTests index 87a65214ab..2ce47ea714 100644 --- a/.github/DockerfileBackendTests +++ b/.github/DockerfileBackendTests @@ -28,7 +28,7 @@ ENV PATH="${PATH}:/usr/local/go/bin" ENV GO_PATH=/usr/local/go/bin/go # UV -RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.11.24/uv-installer.sh | sh && mv /usr/local/cargo/bin/uv /usr/local/bin/uv +RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.13.0/uv-installer.sh | sh && mv /usr/local/cargo/bin/uv /usr/local/bin/uv ENV TZ=Etc/UTC diff --git a/.github/workflows/backend-test-windows.yml b/.github/workflows/backend-test-windows.yml index 73fe57ca6a..3468449753 100644 --- a/.github/workflows/backend-test-windows.yml +++ b/.github/workflows/backend-test-windows.yml @@ -81,7 +81,7 @@ jobs: - uses: astral-sh/setup-uv@v7 with: - version: "0.11.24" + version: "0.13.0" - uses: shivammathur/setup-php@v2 with: diff --git a/.github/workflows/backend-test.yml b/.github/workflows/backend-test.yml index a9a2c573d6..cdfda7e0d4 100644 --- a/.github/workflows/backend-test.yml +++ b/.github/workflows/backend-test.yml @@ -64,7 +64,7 @@ jobs: node-version: "24" - uses: astral-sh/setup-uv@v7 with: - version: "0.11.24" + version: "0.13.0" - uses: shivammathur/setup-php@v2 with: php-version: "8.3" diff --git a/Dockerfile b/Dockerfile index fb4583bf1d..faaa49dd8d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -242,7 +242,7 @@ ENV PATH="${PATH}:/usr/local/go/bin" ENV GO_PATH=/usr/local/go/bin/go # Install UV -RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.11.24/uv-installer.sh | sh && mv /root/.local/bin/uv /usr/local/bin/uv +RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.13.0/uv-installer.sh | sh && mv /root/.local/bin/uv /usr/local/bin/uv # Preinstall python runtimes to temp build location (will copy with world-writable perms later) # --compile-bytecode precompiles the stdlib to .pyc so jobs don't recompile it on every run diff --git a/docker/DockerfileFull b/docker/DockerfileFull index 3528ead3aa..c3a82c8312 100644 --- a/docker/DockerfileFull +++ b/docker/DockerfileFull @@ -15,7 +15,9 @@ RUN RUSTUP_HOME=/usr/local/rustup CARGO_HOME=/usr/local/cargo /usr/local/cargo/b # ansible there leaves its venv interpreter as a dangling symlink at runtime, so every # ansible-* executable fails with ENOENT ("ansible-galaxy not found"). Pin the tool's # interpreter to a persistent image path so the install stays self-contained. -RUN UV_PYTHON_INSTALL_DIR=/usr/local/uv/py uv tool install ansible && [ -d "$(uv tool dir)/ansible/bin/" ] && find "$(uv tool dir)/ansible/bin/" -mindepth 1 -maxdepth 1 -type f -executable -regextype posix-extended -regex '^((.+/)?)[^.]+' -print0 | xargs -0 ln -sf -t "$UV_TOOL_BIN_DIR/" || true +# The version is explicit because uv otherwise downloads its newest stable Python, which +# ansible-core may not support yet: the install succeeds and every executable fails on import. +RUN UV_PYTHON_INSTALL_DIR=/usr/local/uv/py uv tool install --python 3.14 ansible && [ -d "$(uv tool dir)/ansible/bin/" ] && find "$(uv tool dir)/ansible/bin/" -mindepth 1 -maxdepth 1 -type f -executable -regextype posix-extended -regex '^((.+/)?)[^.]+' -print0 | xargs -0 ln -sf -t "$UV_TOOL_BIN_DIR/" || true # C# RUN wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \ diff --git a/docker/DockerfileFullEe b/docker/DockerfileFullEe index 1dd1a1eb6d..bb83195f62 100644 --- a/docker/DockerfileFullEe +++ b/docker/DockerfileFullEe @@ -35,7 +35,9 @@ RUN RUSTUP_HOME=/usr/local/rustup CARGO_HOME=/usr/local/cargo /usr/local/cargo/b # ansible there leaves its venv interpreter as a dangling symlink at runtime, so every # ansible-* executable fails with ENOENT ("ansible-galaxy not found"). Pin the tool's # interpreter to a persistent image path so the install stays self-contained. -RUN UV_PYTHON_INSTALL_DIR=/usr/local/uv/py uv tool install ansible && [ -d "$(uv tool dir)/ansible/bin/" ] && find "$(uv tool dir)/ansible/bin/" -mindepth 1 -maxdepth 1 -type f -executable -regextype posix-extended -regex '^((.+/)?)[^.]+' -print0 | xargs -0 ln -sf -t "$UV_TOOL_BIN_DIR/" || true +# The version is explicit because uv otherwise downloads its newest stable Python, which +# ansible-core may not support yet: the install succeeds and every executable fails on import. +RUN UV_PYTHON_INSTALL_DIR=/usr/local/uv/py uv tool install --python 3.14 ansible && [ -d "$(uv tool dir)/ansible/bin/" ] && find "$(uv tool dir)/ansible/bin/" -mindepth 1 -maxdepth 1 -type f -executable -regextype posix-extended -regex '^((.+/)?)[^.]+' -print0 | xargs -0 ln -sf -t "$UV_TOOL_BIN_DIR/" || true # dotnet SDK RUN wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \ && chmod +x dotnet-install.sh \ diff --git a/docker/DockerfileSlim b/docker/DockerfileSlim index 45986bbd5f..a37ced0eca 100644 --- a/docker/DockerfileSlim +++ b/docker/DockerfileSlim @@ -58,7 +58,7 @@ RUN curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmo ENV TZ=Etc/UTC # Install UV -RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.11.24/uv-installer.sh | sh && mv /root/.local/bin/uv /usr/local/bin/uv +RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.13.0/uv-installer.sh | sh && mv /root/.local/bin/uv /usr/local/bin/uv # Preinstall python runtime to temp location (will copy with world-writable perms later) # --compile-bytecode precompiles the stdlib to .pyc so jobs don't recompile it on every run diff --git a/docker/DockerfileSlimEe b/docker/DockerfileSlimEe index 5ac24f5c1d..687b1506ce 100644 --- a/docker/DockerfileSlimEe +++ b/docker/DockerfileSlimEe @@ -58,7 +58,7 @@ RUN curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmo ENV TZ=Etc/UTC # Install UV -RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.11.24/uv-installer.sh | sh && mv /root/.local/bin/uv /usr/local/bin/uv +RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.13.0/uv-installer.sh | sh && mv /root/.local/bin/uv /usr/local/bin/uv # Preinstall python runtime to temp location (will copy with world-writable perms later) # --compile-bytecode precompiles the stdlib to .pyc so jobs don't recompile it on every run From 0758bdc46b3c8391914f18718730ee8cbf18cf6a Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sun, 11 Oct 2026 11:22:46 +0200 Subject: [PATCH 14/20] refactor: remove the frontend build-time base url (#11674) Co-authored-by: Claude Opus 5.5 --- Dockerfile | 1 - frontend/src/lib/apiBaseUrl.svelte.ts | 3 +- frontend/src/lib/base.ts | 1 - frontend/src/lib/components/AgentTrace.svelte | 3 +- .../src/lib/components/AppConnectInner.svelte | 3 +- frontend/src/lib/components/ArgInput.svelte | 3 +- .../src/lib/components/CiTestResults.svelte | 5 ++- .../lib/components/CompareWorkspaces.svelte | 3 +- .../src/lib/components/DeployWorkspace.svelte | 5 ++- .../lib/components/DevWorkspaceSetting.svelte | 5 ++- .../src/lib/components/DisplayResult.svelte | 4 +-- .../components/DisplayResultControlBar.svelte | 4 +-- frontend/src/lib/components/EditorBar.svelte | 7 ++--- .../components/ErrorOrRecoveryHandler.svelte | 7 ++--- .../src/lib/components/FlowLogViewer.svelte | 3 +- .../src/lib/components/FlowMetadata.svelte | 9 +++--- .../components/FlowStatusViewerInner.svelte | 5 ++- .../src/lib/components/GitRepoViewer.svelte | 5 ++- frontend/src/lib/components/LogViewer.svelte | 3 +- frontend/src/lib/components/Login.svelte | 7 ++--- .../src/lib/components/NavBreadcrumb.svelte | 5 ++- .../components/ParqetCsvTableRenderer.svelte | 3 +- .../components/PersistentScriptDrawer.svelte | 5 ++- frontend/src/lib/components/RunsPage.svelte | 4 +-- .../lib/components/S3FilePickerInner.svelte | 5 ++- .../src/lib/components/ScriptBuilder.svelte | 3 +- .../src/lib/components/ScriptEditor.svelte | 5 ++- .../src/lib/components/ScriptPicker.svelte | 9 +++--- .../components/SuperadminSettingsInner.svelte | 11 +++---- .../src/lib/components/TimelineBar.svelte | 3 +- .../components/UpdateDevWorkspaceModal.svelte | 3 +- .../lib/components/WorkflowTimeline.svelte | 3 +- .../lib/components/aiEvals/EvalScorers.svelte | 5 ++- .../lib/components/aiEvals/EvalsPane.svelte | 5 ++- .../apps/components/helpers/eval.ts | 5 ++- .../apps/editor/AppEditorHeaderDeploy.svelte | 3 +- .../apps/editor/AppReportsDrawerInner.svelte | 3 +- .../apps/editor/InWorkspaceAppViewer.svelte | 7 ++--- .../apps/editor/PublicAppAccess.svelte | 3 +- .../apps/editor/PublicAppFrame.svelte | 3 +- .../apps/editor/appDeploy.svelte.ts | 3 +- .../InlineScriptRunnableByPath.svelte | 5 ++- .../AssetGraph/AssetGraphDetailsPane.svelte | 7 ++--- .../assets/AssetGraph/AssetRunsPanel.svelte | 3 +- .../assets/AssetGraph/DataTablePreview.svelte | 3 +- .../AssetGraph/PipelineActivityPanel.svelte | 3 +- .../AssetGraph/PipelineFolderList.svelte | 3 +- .../AssetGraph/PipelineSetupSignpost.svelte | 5 ++- .../DraftEditorModals.svelte | 3 +- .../OtherUsersDraftsModal.svelte | 3 +- .../common/fileDownload/FileDownload.svelte | 3 +- .../common/slack/SlackConnectionStatus.svelte | 5 ++- .../components/common/table/AgentRow.svelte | 5 ++- .../lib/components/common/table/AppRow.svelte | 9 +++--- .../components/common/table/FlowRow.svelte | 13 ++++---- .../common/table/PipelineRow.svelte | 3 +- .../components/common/table/RawAppRow.svelte | 3 +- .../components/common/table/ScriptRow.svelte | 13 ++++---- .../common/teams/TeamsConnectionStatus.svelte | 3 +- .../components/copilot/AIFormAssistant.svelte | 3 +- .../src/lib/components/copilot/CronGen.svelte | 3 +- .../copilot/FlowInlineScriptAIButton.svelte | 3 +- .../lib/components/copilot/RegexGen.svelte | 3 +- .../lib/components/copilot/ResourceGen.svelte | 3 +- .../lib/components/copilot/ScriptFix.svelte | 3 +- .../lib/components/copilot/ScriptGen.svelte | 3 +- .../components/copilot/StepInputsGen.svelte | 3 +- .../components/copilot/chat/AIButton.svelte | 3 +- .../lib/components/copilot/chat/AIChat.svelte | 3 +- .../copilot/chat/AIChatDisplay.svelte | 5 ++- .../copilot/chat/AIChatManager.test.ts | 1 - .../copilot/chat/AIChatModelSettings.svelte | 3 +- .../chat/AssistantInstructionsSection.svelte | 3 +- .../copilot/chat/AssistantMessage.svelte | 3 +- .../copilot/chat/JobsSegment.svelte | 3 +- .../copilot/chat/RunScriptCard.svelte | 3 +- .../copilot/chat/ToolExecutionDisplay.svelte | 3 +- .../chat/artifacts/artifactSharing.test.ts | 2 -- .../copilot/chat/artifacts/artifactSharing.ts | 4 +-- .../components/copilot/chat/global/core.ts | 5 ++- .../lib/components/dbt/DbtModelGraph.svelte | 5 ++- .../lib/components/details/FlowDetail.svelte | 7 ++--- .../components/details/ScriptDetail.svelte | 9 +++--- .../components/flows/CreateActionsApp.svelte | 5 ++- .../components/flows/CreateActionsFlow.svelte | 11 +++---- .../flows/content/ExpandedSubflowStep.svelte | 3 +- .../flows/content/FlowEditorDrawer.svelte | 3 +- .../flows/conversations/messageAttachments.ts | 3 +- .../flows/propPicker/OutputBadge.svelte | 3 +- .../flows/propPicker/OutputPickerInner.svelte | 3 -- .../components/git_sync/GitSyncSection.svelte | 3 +- .../components/home/CreateActionsMenu.svelte | 31 +++++++++---------- .../src/lib/components/home/HomeAIChat.svelte | 3 +- .../jobs/MissingWorkerTagAlert.svelte | 3 +- .../src/lib/components/mcp/McpConnect.svelte | 3 +- .../components/otherUserDraftLoad.svelte.ts | 9 +++--- .../preview/FlowPreviewStatus.svelte | 3 +- .../raw_apps/RawAppEditorHeader.svelte | 7 ++--- .../raw_apps/RawAppTemplatePicker.svelte | 7 ++--- .../runs/DispatchEventsTable.svelte | 3 +- .../components/runs/JobDetailHeader.svelte | 15 +++++---- .../src/lib/components/runs/RunBadges.svelte | 7 ++--- .../src/lib/components/runs/RunRow.svelte | 7 ++--- .../components/schedules/SchedulesList.svelte | 11 +++---- .../components/schema/JobSchemaPicker.svelte | 3 +- .../schema/RunningJobSchemaPicker.svelte | 3 +- .../components/scriptEditor/LogPanel.svelte | 3 +- .../scripts/CreateActionsScript.svelte | 3 +- .../components/sessions/ItemViewerView.svelte | 7 ++--- .../components/sessions/PageListView.svelte | 2 +- .../sessions/PreviewRouterPicker.svelte | 5 ++- .../sessions/RawAppEditorView.svelte | 3 +- .../sessions/SessionModeSwitch.svelte | 3 +- .../components/sessions/SessionPicker.svelte | 3 +- .../sessions/SessionsBetaBanner.svelte | 3 +- .../components/sessions/pageDrawerSession.ts | 11 +++---- .../lib/components/sessions/previewPaths.ts | 26 +++++++--------- .../lib/components/sessions/previewReload.ts | 6 ++-- .../lib/components/sessions/previewRouter.ts | 27 +++++++--------- .../components/sessions/sessionMode.svelte.ts | 7 ++--- .../sessions/sessionPreviewTabs.svelte.ts | 13 ++++---- .../sessions/sessionPreviewTabs.test.ts | 25 +++++++-------- .../sessions/sessionRuntime.svelte.ts | 5 ++- .../components/settings/PremiumInfo.svelte | 5 ++- .../sidebar/CriticalAlertModal.svelte | 7 ++--- .../sidebar/FinishAccountSetup.svelte | 3 +- .../lib/components/sidebar/MenuLink.svelte | 3 +- .../components/sidebar/SettingsMenu.svelte | 15 +++++---- .../components/sidebar/SidebarContent.svelte | 15 +++++---- .../sidebar/WorkspacePickerBody.svelte | 7 ++--- .../sidebar/WorkspaceScopeHeader.svelte | 3 +- .../lib/components/sidebar/navPages.test.ts | 12 +++---- .../src/lib/components/sidebar/navPages.ts | 3 +- .../components/sidebar/workspaceMenuHref.ts | 4 +-- .../components/triggers/CaptureSection.svelte | 2 -- .../triggers/TriggerEditorToolbar.svelte | 4 +-- .../components/triggers/TriggerList.svelte | 5 ++- .../azure/AzureTriggerEditorInner.svelte | 3 +- .../components/triggers/gcp/GcpCapture.svelte | 3 +- .../triggers/gcp/GcpTriggerEditorInner.svelte | 3 +- .../triggers/http/RouteCapture.svelte | 3 +- .../PostgresTriggerEditorInner.svelte | 3 +- .../scheduled/ScheduledPollPanel.svelte | 5 ++- .../schedules/ScheduleEditorInner.svelte | 3 +- .../CreateWorkspaceInner.svelte | 3 +- .../workspaceSettings/DucklakeSettings.svelte | 3 +- .../deployToHubSession.svelte.ts | 5 ++- .../workspaceSettings/supabaseOauth.svelte.ts | 3 +- .../workspaceSettings/supabaseProvisioning.ts | 11 +++---- frontend/src/lib/draftAddRedirect.ts | 5 ++- frontend/src/lib/githubApp.ts | 3 +- frontend/src/lib/navigation.test.ts | 3 +- frontend/src/lib/navigation.ts | 15 ++------- frontend/src/lib/remoteDeploy.ts | 3 +- frontend/src/lib/scripts.ts | 5 ++- frontend/src/lib/utils/editInFork.ts | 11 +++---- frontend/src/lib/workspaceCreation.ts | 3 +- .../src/routes/(root)/(logged)/+layout.svelte | 15 +++++---- .../src/routes/(root)/(logged)/+page.svelte | 7 ++--- .../agents/edit/[...path]/+page.svelte | 5 ++- .../agents/get/[...path]/+page.svelte | 7 ++--- .../get_raw/[version]/[...path]/+page.svelte | 3 +- .../(logged)/concurrency_groups/+page.svelte | 3 +- .../(logged)/pipeline/[folder]/+page.svelte | 7 ++--- .../projects/import/+page@(root).svelte | 3 +- .../(root)/(logged)/projects/install/+page.ts | 3 +- .../(root)/(logged)/run/[...run]/+page.svelte | 9 +++--- .../(root)/(logged)/sessions/+page.svelte | 17 +++++----- .../user/(user)/accept_invite/+page.svelte | 3 +- .../(logged)/user/(user)/login/+page.svelte | 3 +- .../user/(user)/workspaces/+page.svelte | 7 ++--- .../(root)/(logged)/user/cli/+page.svelte | 3 +- .../user/remote_deploy_authorize/+page.svelte | 3 +- .../(logged)/workspace_settings/+page.svelte | 23 +++++++------- frontend/src/routes/(root)/+layout.svelte | 5 ++- .../approve/[workspace]/[job]/+page.svelte | 3 +- .../[job]/[resume]/[hmac]/+page.svelte | 3 +- .../public_run/[workspace]/[id]/+page.svelte | 3 +- frontend/src/routes/a/[...path]/+page.svelte | 3 +- .../[workspace]/[...secret]/+page.svelte | 3 +- frontend/svelte.config.js | 3 -- 181 files changed, 393 insertions(+), 596 deletions(-) delete mode 100644 frontend/src/lib/base.ts diff --git a/Dockerfile b/Dockerfile index faaa49dd8d..c6a4ccc55f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -83,7 +83,6 @@ COPY /python-client/docs/ /frontend/static/pydocs/ RUN npm run generate-backend-client ENV NODE_OPTIONS "--max-old-space-size=8192" -ARG VITE_BASE_URL "" # Must be declared for the build-arg to reach the bundle. See frontend/svelte.config.js. ARG WM_BUILD_VERSION="" # Read more about macro in docker/dev.nu diff --git a/frontend/src/lib/apiBaseUrl.svelte.ts b/frontend/src/lib/apiBaseUrl.svelte.ts index f87a33f0d8..242a6aa9e0 100644 --- a/frontend/src/lib/apiBaseUrl.svelte.ts +++ b/frontend/src/lib/apiBaseUrl.svelte.ts @@ -1,5 +1,4 @@ import { fromStore } from 'svelte/store' -import { base } from '$lib/base' import { instanceUi } from '$lib/instanceUi' const ui = fromStore(instanceUi) @@ -18,5 +17,5 @@ const ui = fromStore(instanceUi) export function apiBaseUrl(): string { const setting = ui.current?.api_base_url const configured = typeof setting === 'string' ? setting.trim() : '' - return configured || `${window.location.origin}${base}` + return configured || window.location.origin } diff --git a/frontend/src/lib/base.ts b/frontend/src/lib/base.ts deleted file mode 100644 index 0e8f2d71f6..0000000000 --- a/frontend/src/lib/base.ts +++ /dev/null @@ -1 +0,0 @@ -export let base: string = import.meta.env.VITE_BASE_URL ?? '' diff --git a/frontend/src/lib/components/AgentTrace.svelte b/frontend/src/lib/components/AgentTrace.svelte index 7f2a90e621..3d671f46c8 100644 --- a/frontend/src/lib/components/AgentTrace.svelte +++ b/frontend/src/lib/components/AgentTrace.svelte @@ -1,7 +1,6 @@ diff --git a/frontend/src/lib/components/common/confirmationModal/OtherUsersDraftsModal.svelte b/frontend/src/lib/components/common/confirmationModal/OtherUsersDraftsModal.svelte index ce9b07768e..b6ec78f336 100644 --- a/frontend/src/lib/components/common/confirmationModal/OtherUsersDraftsModal.svelte +++ b/frontend/src/lib/components/common/confirmationModal/OtherUsersDraftsModal.svelte @@ -22,7 +22,6 @@ import { UserDraft } from '$lib/userDraft.svelte' import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte' import { goto } from '$app/navigation' - import { base } from '$app/paths' export type OtherDraftUser = { username?: string | null; draft_saved_at?: string } @@ -115,7 +114,7 @@ target === 'all' || (!hasOwnDraft && otherDraftsUsers.every((o) => ownerKey(o) === ownerKey(target))) if (draftOnly && lastDraftGone) { - await goto(`${base}/`) + await goto('/') return } try { diff --git a/frontend/src/lib/components/common/fileDownload/FileDownload.svelte b/frontend/src/lib/components/common/fileDownload/FileDownload.svelte index c1a29fae70..7f9187da62 100644 --- a/frontend/src/lib/components/common/fileDownload/FileDownload.svelte +++ b/frontend/src/lib/components/common/fileDownload/FileDownload.svelte @@ -1,6 +1,5 @@ diff --git a/frontend/src/lib/components/common/table/AgentRow.svelte b/frontend/src/lib/components/common/table/AgentRow.svelte index 54142a4822..aca6ae7f9c 100644 --- a/frontend/src/lib/components/common/table/AgentRow.svelte +++ b/frontend/src/lib/components/common/table/AgentRow.svelte @@ -1,5 +1,4 @@ {#snippet chatBadge()} diff --git a/frontend/src/lib/components/common/table/AppRow.svelte b/frontend/src/lib/components/common/table/AppRow.svelte index 50bc64f24b..a318c1f412 100644 --- a/frontend/src/lib/components/common/table/AppRow.svelte +++ b/frontend/src/lib/components/common/table/AppRow.svelte @@ -1,5 +1,4 @@ import { goto } from '$lib/navigation' - import { base } from '$lib/base' import Dropdown from '$lib/components/DropdownV2.svelte' import type MoveDrawer from '$lib/components/MoveDrawer.svelte' import ScheduleEditor from '$lib/components/triggers/schedules/ScheduleEditor.svelte' @@ -157,8 +156,8 @@ aiId={`script-run-button-${script.path}`} aiDescription={`Button to access the form to run the script ${script.summary ?? script.path}`} href={script.draft_only || (script.auto_kind === 'lib' && script.kind !== 'preprocessor') - ? `${base}/scripts/edit/${script.path}` - : `${base}/scripts/get/${script.hash}?workspace=${$workspaceStore}`} + ? `/scripts/edit/${script.path}` + : `/scripts/get/${script.hash}?workspace=${$workspaceStore}`} kind="script" {keyboardSelected} {marked} @@ -261,7 +260,7 @@ wrapperClasses="w-20" unifiedSize="md" startIcon={{ icon: Pen }} - href="{base}/scripts/edit/{script.path}" + href="/scripts/edit/{script.path}" > Edit @@ -351,7 +350,7 @@ { displayName: 'Duplicate/Fork', icon: GitFork, - href: `${base}/scripts/add?template=${script.path}`, + href: `/scripts/add?template=${script.path}`, disabled: !showEditButton, hide: $userStore?.operator }, @@ -391,7 +390,7 @@ { displayName: 'View runs', icon: List, - href: `${base}/runs/${script.path}` + href: `/runs/${script.path}` }, { displayName: 'Versions', @@ -403,7 +402,7 @@ { displayName: 'Audit logs', icon: Eye, - href: `${base}/audit_logs?resource=${script.path}`, + href: `/audit_logs?resource=${script.path}`, hide: $userStore?.operator }, { diff --git a/frontend/src/lib/components/common/teams/TeamsConnectionStatus.svelte b/frontend/src/lib/components/common/teams/TeamsConnectionStatus.svelte index ce368e93b9..64b9269f3d 100644 --- a/frontend/src/lib/components/common/teams/TeamsConnectionStatus.svelte +++ b/frontend/src/lib/components/common/teams/TeamsConnectionStatus.svelte @@ -1,7 +1,6 @@ diff --git a/frontend/src/lib/components/copilot/AIFormAssistant.svelte b/frontend/src/lib/components/copilot/AIFormAssistant.svelte index b4526f1153..48a46ad182 100644 --- a/frontend/src/lib/components/copilot/AIFormAssistant.svelte +++ b/frontend/src/lib/components/copilot/AIFormAssistant.svelte @@ -8,7 +8,6 @@ import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' import { copilotInfo } from '$lib/aiStore' import { logFeatureUsage } from '$lib/utils/featureUsage' - import { pageHref } from '$lib/components/sessions/previewPaths' interface Props { /** Unset for a viewer who cannot edit the item: the card then offers no way to the editor. */ @@ -45,7 +44,7 @@ const sessionSource = $derived( path ? { - page: () => pageHref(`/${runnableType}s/get/${path}`), + page: () => `/${runnableType}s/get/${path}`, workspaceId: $operatingWorkspace ?? undefined, beforeOpen: logAsked } diff --git a/frontend/src/lib/components/copilot/CronGen.svelte b/frontend/src/lib/components/copilot/CronGen.svelte index 94715a0f18..fb5806234c 100644 --- a/frontend/src/lib/components/copilot/CronGen.svelte +++ b/frontend/src/lib/components/copilot/CronGen.svelte @@ -5,7 +5,6 @@ import Popover from '$lib/components/meltComponents/Popover.svelte' import { sendUserToast } from '$lib/toast' - import { base } from '$lib/base' import type { ChatCompletionMessageParam } from 'openai/resources/index.mjs' import { copilotInfo } from '$lib/aiStore' import { untrack } from 'svelte' @@ -132,7 +131,7 @@

Enable Windmill AI in the workspace settings

Enable Windmill AI in the diff --git a/frontend/src/lib/components/copilot/RegexGen.svelte b/frontend/src/lib/components/copilot/RegexGen.svelte index f399726988..32fdc4a5f1 100644 --- a/frontend/src/lib/components/copilot/RegexGen.svelte +++ b/frontend/src/lib/components/copilot/RegexGen.svelte @@ -1,7 +1,6 @@ diff --git a/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte b/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte index 38f91353a2..832a96d6b4 100644 --- a/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte +++ b/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte @@ -13,7 +13,6 @@ ExternalLink, BookOpen } from 'lucide-svelte' - import { base } from '$lib/base' import { truncateRev } from '$lib/utils' import { EXIT_PLAN_MODE_TOOL, @@ -301,7 +300,7 @@ {#snippet jobLink()} ({ base: '' })) - import { shareWorkspaceId } from './artifactSharing' describe('shareWorkspaceId', () => { diff --git a/frontend/src/lib/components/copilot/chat/artifacts/artifactSharing.ts b/frontend/src/lib/components/copilot/chat/artifacts/artifactSharing.ts index 57b08cba9b..269eb6354b 100644 --- a/frontend/src/lib/components/copilot/chat/artifacts/artifactSharing.ts +++ b/frontend/src/lib/components/copilot/chat/artifacts/artifactSharing.ts @@ -1,5 +1,3 @@ -import { base } from '$lib/base' - /** * The workspace a share from `workspaceId` lands in: the topmost ancestor the user still * belongs to. A session often runs in a fork, whose members are its creator alone, so a link @@ -20,7 +18,7 @@ export function shareWorkspaceId( } export function sharedArtifactUrl(workspaceId: string, shareId: string): string { - return `${window.location.origin}${base}/shared_artifacts/${encodeURIComponent( + return `${window.location.origin}/shared_artifacts/${encodeURIComponent( shareId )}?workspace=${encodeURIComponent(workspaceId)}` } diff --git a/frontend/src/lib/components/copilot/chat/global/core.ts b/frontend/src/lib/components/copilot/chat/global/core.ts index b6eac0439c..2a795f1f81 100644 --- a/frontend/src/lib/components/copilot/chat/global/core.ts +++ b/frontend/src/lib/components/copilot/chat/global/core.ts @@ -246,7 +246,6 @@ import { parseItemsMaskParam } from '$lib/components/sessions/modifiedItemsMask' import { - pageHref, TRIGGER_PAGES, type TriggerKind as PageTriggerKind } from '$lib/components/sessions/previewRouter' @@ -336,7 +335,7 @@ export type GlobalActiveEditorContext = { export type GlobalActivePreviewContext = { /** Page name as the tab strip shows it, e.g. "Schedules". */ label: string - /** Base-stripped page path plus the request params the page declares — values kept + /** Page path plus the request params the page declares — values kept * only for the ones addressing a workspace object, and percent-encoded. Never a raw * location: a tab can host a legacy app whose hash is app state, and a filter value * can be free text the user typed. Build it with `previewLocationContext`. */ @@ -3461,7 +3460,7 @@ export const openPageTool: SessionTool<{}> = { // is no active session, in which case we offer a link chip instead. const previewResult = openPagePreview({ sessionId: sessionIdFromCtx(ctx), - href: pageHref(url), + href: url, label: pageLabel, newTab: parsed.new_tab ?? false }) diff --git a/frontend/src/lib/components/dbt/DbtModelGraph.svelte b/frontend/src/lib/components/dbt/DbtModelGraph.svelte index bd7f42a9e7..12b008812d 100644 --- a/frontend/src/lib/components/dbt/DbtModelGraph.svelte +++ b/frontend/src/lib/components/dbt/DbtModelGraph.svelte @@ -16,7 +16,6 @@ import { Button } from '$lib/components/common' import { Loader2, RefreshCw } from 'lucide-svelte' import { displayDate } from '$lib/utils' - import { base } from '$lib/base' import AssetGraphCanvas from '$lib/components/assets/AssetGraph/AssetGraphCanvas.svelte' import type { DbtPreviewBuffer } from './previewRows' import type { @@ -477,7 +476,7 @@ worker tag nothing serves waits here indefinitely. Open the parse job @@ -491,7 +490,7 @@ {#if refreshError.job} Open the parse job diff --git a/frontend/src/lib/components/details/FlowDetail.svelte b/frontend/src/lib/components/details/FlowDetail.svelte index c755136748..30de5dc7cb 100644 --- a/frontend/src/lib/components/details/FlowDetail.svelte +++ b/frontend/src/lib/components/details/FlowDetail.svelte @@ -23,7 +23,6 @@ import DetailPageLayout from '$lib/components/details/DetailPageLayout.svelte' import OnBehalfOfBadge from '$lib/components/details/OnBehalfOfBadge.svelte' import { goto } from '$lib/navigation' - import { base } from '$lib/base' import { Badge as HeaderBadge, Alert } from '$lib/components/common' import MoveDrawer from '$lib/components/MoveDrawer.svelte' import RunForm from '$lib/components/RunForm.svelte' @@ -372,7 +371,7 @@ description: `Start a new flow from a copy of this one`, narrow: { dropdownOf: 'Edit' }, buttonProps: { - href: operatingHref(`${base}/flows/add?template=${flow.path}`), + href: operatingHref(`/flows/add?template=${flow.path}`), onClick: interceptNav(onNavigate, `/flows/add?template=${flow.path}`), variant: 'subtle', unifiedSize: 'md', @@ -409,7 +408,7 @@ buttons.push({ label: `Runs`, buttonProps: { - href: operatingHref(`${base}/runs/${flow.path}`), + href: operatingHref(`/runs/${flow.path}`), onClick: interceptNav(onNavigate, `/runs/${flow.path}`), unifiedSize: 'md', variant: 'subtle', @@ -455,7 +454,7 @@ buttons.push({ label: 'Edit', buttonProps: { - href: operatingHref(`${base}/flows/edit/${path}`), + href: operatingHref(`/flows/edit/${path}`), onClick: interceptNav(onNavigate, `/flows/edit/${path}`), variant: 'accent', unifiedSize: 'md', diff --git a/frontend/src/lib/components/details/ScriptDetail.svelte b/frontend/src/lib/components/details/ScriptDetail.svelte index e8536aed91..6712092c39 100644 --- a/frontend/src/lib/components/details/ScriptDetail.svelte +++ b/frontend/src/lib/components/details/ScriptDetail.svelte @@ -1,5 +1,4 @@ diff --git a/frontend/src/lib/components/flows/CreateActionsFlow.svelte b/frontend/src/lib/components/flows/CreateActionsFlow.svelte index ae848156f5..9f78585cc0 100644 --- a/frontend/src/lib/components/flows/CreateActionsFlow.svelte +++ b/frontend/src/lib/components/flows/CreateActionsFlow.svelte @@ -1,6 +1,5 @@ diff --git a/frontend/src/lib/components/flows/content/ExpandedSubflowStep.svelte b/frontend/src/lib/components/flows/content/ExpandedSubflowStep.svelte index 979717dae1..9002e88adf 100644 --- a/frontend/src/lib/components/flows/content/ExpandedSubflowStep.svelte +++ b/frontend/src/lib/components/flows/content/ExpandedSubflowStep.svelte @@ -13,7 +13,6 @@ type ResolvedExpandedSubflowStep } from '../expandedSubflowStep' import { parseExpandedSubflowId } from '$lib/components/restartFromStepPath' - import { base } from '$app/paths' import FlowPanelChrome from '../common/FlowPanelChrome.svelte' import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' @@ -122,7 +121,7 @@ title="Open the subflow in a new tab" startIcon={{ icon: ExternalLink }} iconOnly - href={`${base}/flows/edit/${containingFlowPath}?workspace=${encodeURIComponent( + href={`/flows/edit/${containingFlowPath}?workspace=${encodeURIComponent( opWs ?? '' )}${module ? `&selected=${encodeURIComponent(leafId)}` : ''}`} target="_blank" diff --git a/frontend/src/lib/components/flows/content/FlowEditorDrawer.svelte b/frontend/src/lib/components/flows/content/FlowEditorDrawer.svelte index 6b4d73c6a0..c6c1d13b2a 100644 --- a/frontend/src/lib/components/flows/content/FlowEditorDrawer.svelte +++ b/frontend/src/lib/components/flows/content/FlowEditorDrawer.svelte @@ -11,7 +11,6 @@ import { initFlow } from '$lib/components/flows/flowStore.svelte' import type { FlowState } from '$lib/components/flows/flowState' import type { FlowEditorContext } from '../types' - import { base } from '$app/paths' import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' const operatingWorkspace = useOperatingWorkspace() @@ -127,7 +126,7 @@ // Follow the drawer's own selection, which the user may have moved since it opened. const stepId = flowBuilder?.getSelectedId() ?? selectedStepId const selected = stepId ? `?selected=${encodeURIComponent(stepId)}` : '' - window.open(`${base}/flows/edit/${flowPath}${selected}`, '_blank', 'noopener,noreferrer') + window.open(`/flows/edit/${flowPath}${selected}`, '_blank', 'noopener,noreferrer') flowEditorDrawer?.closeDrawer() }} startIcon={{ icon: ExternalLink }} diff --git a/frontend/src/lib/components/flows/conversations/messageAttachments.ts b/frontend/src/lib/components/flows/conversations/messageAttachments.ts index 36c7d103c3..18170a90df 100644 --- a/frontend/src/lib/components/flows/conversations/messageAttachments.ts +++ b/frontend/src/lib/components/flows/conversations/messageAttachments.ts @@ -1,6 +1,5 @@ /** The files a user message carried, as the lanes its bubble renders: image thumbnails and file chips. */ import type { ChatAttachment } from 'windmill-chat' -import { base } from '$lib/base' import { createAttachedFileContextElement, type ContextElement @@ -24,7 +23,7 @@ function looksLikeImage(attachment: ChatAttachment): boolean { function downloadUrl(workspace: string, attachment: ChatAttachment): string { const params = new URLSearchParams({ file_key: attachment.s3 }) if (attachment.storage) params.set('storage', attachment.storage) - return `${base}/api/w/${workspace}/job_helpers/download_s3_file?${params.toString()}` + return `/api/w/${workspace}/job_helpers/download_s3_file?${params.toString()}` } /** Without a workspace there is no link to build, so an image falls back to a file chip. */ diff --git a/frontend/src/lib/components/flows/propPicker/OutputBadge.svelte b/frontend/src/lib/components/flows/propPicker/OutputBadge.svelte index 507e74ba86..ecc8e07b11 100644 --- a/frontend/src/lib/components/flows/propPicker/OutputBadge.svelte +++ b/frontend/src/lib/components/flows/propPicker/OutputBadge.svelte @@ -1,7 +1,6 @@ diff --git a/frontend/src/lib/components/sessions/SessionPicker.svelte b/frontend/src/lib/components/sessions/SessionPicker.svelte index d4f7fa66ab..7e8283e406 100644 --- a/frontend/src/lib/components/sessions/SessionPicker.svelte +++ b/frontend/src/lib/components/sessions/SessionPicker.svelte @@ -66,7 +66,6 @@ import { sendUserToast } from '$lib/toast' import { currentWorkspaceRootId, workspaceRootId } from './sessionScope.svelte' import { page } from '$app/state' - import { base } from '$app/paths' import DevWorkspaceBadge from '$lib/components/DevWorkspaceBadge.svelte' import { dateBucket, @@ -119,7 +118,7 @@ // Only highlight the active session while the sessions page is open — elsewhere // `currentSessionId` lingers but no row should appear selected. - const onSessionsPage = $derived(page.url.pathname.startsWith(`${base}/sessions`)) + const onSessionsPage = $derived(page.url.pathname.startsWith('/sessions')) const sessionActive = $derived(onSessionsPage) interface Props { diff --git a/frontend/src/lib/components/sessions/SessionsBetaBanner.svelte b/frontend/src/lib/components/sessions/SessionsBetaBanner.svelte index 86b6ba030a..0d523a4e65 100644 --- a/frontend/src/lib/components/sessions/SessionsBetaBanner.svelte +++ b/frontend/src/lib/components/sessions/SessionsBetaBanner.svelte @@ -1,5 +1,4 @@ @@ -17,7 +16,7 @@ 1. Choose a trigger script using the script picker that will poll for new events regularly script picker @@ -26,7 +25,7 @@ 2. Add logic to process each new event create action diff --git a/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte b/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte index 3c0982cf9b..141e67fd1b 100644 --- a/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte +++ b/frontend/src/lib/components/triggers/schedules/ScheduleEditorInner.svelte @@ -35,7 +35,6 @@ } from '$lib/gen' import { emptyString, formatCron, sendUserToast, cronV1toV2 } from '$lib/utils' import { useEditRights } from '$lib/operatorWriteRights' - import { base } from '$lib/base' import Section from '$lib/components/Section.svelte' import { List, Loader2, AlertTriangle } from 'lucide-svelte' import autosize from '$lib/autosize' @@ -655,7 +654,7 @@ variant="default" startIcon={{ icon: List }} disabled={!allowSchedule || pathError != '' || emptyString(script_path)} - href={`${base}/runs/?schedule_path=${path}&job_trigger_kind=schedule&show_future_jobs=true`} + href={`/runs/?schedule_path=${path}&job_trigger_kind=schedule&show_future_jobs=true`} > View runs diff --git a/frontend/src/lib/components/workspaceSettings/CreateWorkspaceInner.svelte b/frontend/src/lib/components/workspaceSettings/CreateWorkspaceInner.svelte index 5ae952f03e..959b666363 100644 --- a/frontend/src/lib/components/workspaceSettings/CreateWorkspaceInner.svelte +++ b/frontend/src/lib/components/workspaceSettings/CreateWorkspaceInner.svelte @@ -64,7 +64,6 @@ import TextInput from '../text_input/TextInput.svelte' import { jobManager } from '$lib/services/JobManager' import Alert from '../common/alert/Alert.svelte' - import { base } from '$lib/base' import Label from '../Label.svelte' import ForkDatatableSection from './ForkDatatableSection.svelte' import Select from '../select/Select.svelte' @@ -1106,7 +1105,7 @@ disabled={forkCreationLoading} variant="default" size="sm" - href="{base}/user/workspaces">← Back to workspaces← Back to workspaces {/if} {#if !forkCreationLoading} diff --git a/frontend/src/lib/components/workspaceSettings/DucklakeSettings.svelte b/frontend/src/lib/components/workspaceSettings/DucklakeSettings.svelte index f6929e0779..a01318d916 100644 --- a/frontend/src/lib/components/workspaceSettings/DucklakeSettings.svelte +++ b/frontend/src/lib/components/workspaceSettings/DucklakeSettings.svelte @@ -91,7 +91,6 @@ import type { GetSettingsResponse } from '$lib/gen' import { enterpriseLicense, superadmin, userWorkspaces, workspaceStore } from '$lib/stores' - import { base } from '$app/paths' import Toggle from '../Toggle.svelte' import { sendUserToast } from '$lib/toast' import ExploreAssetButton from '../ExploreAssetButton.svelte' @@ -653,7 +652,7 @@ {/if} {#if ducklakeSavedSettings.ducklakes.find((d) => d.name === ducklake.name)?.maintenance?.enabled} ) { diff --git a/frontend/src/lib/components/workspaceSettings/supabaseOauth.svelte.ts b/frontend/src/lib/components/workspaceSettings/supabaseOauth.svelte.ts index 3e11ec2b9d..94c21cf591 100644 --- a/frontend/src/lib/components/workspaceSettings/supabaseOauth.svelte.ts +++ b/frontend/src/lib/components/workspaceSettings/supabaseOauth.svelte.ts @@ -1,9 +1,8 @@ import { fromStore } from 'svelte/store' -import { base } from '$lib/base' import { oauthStore } from '$lib/stores' const OAUTH_WINDOW = 'windmill_supabase_oauth' -const CONNECT_URL = `${base}/api/oauth/connect/supabase_wizard` +const CONNECT_URL = '/api/oauth/connect/supabase_wizard' /** * The Supabase authorization leg, driven from a popup. diff --git a/frontend/src/lib/components/workspaceSettings/supabaseProvisioning.ts b/frontend/src/lib/components/workspaceSettings/supabaseProvisioning.ts index 5040b5ab38..ee91a0ba3c 100644 --- a/frontend/src/lib/components/workspaceSettings/supabaseProvisioning.ts +++ b/frontend/src/lib/components/workspaceSettings/supabaseProvisioning.ts @@ -7,7 +7,6 @@ */ import { DEFAULT_SSLMODE } from '$lib/utils/postgresConnectionString' -import { base } from '$lib/base' import { oauthStore } from '$lib/stores' import { get } from 'svelte/store' @@ -90,19 +89,19 @@ export function supabaseErrorMessage(body: string): string { } export async function listSupabaseOrgs(token: string): Promise { - const res = await fetch(`${base}/api/oauth/list_supabase_orgs`, { headers: headers(token) }) + const res = await fetch('/api/oauth/list_supabase_orgs', { headers: headers(token) }) return unwrap(res, 'Could not list your Supabase organizations') } export async function listSupabaseProjects(token: string): Promise { - const res = await fetch(`${base}/api/oauth/list_supabase`, { headers: headers(token) }) + const res = await fetch('/api/oauth/list_supabase', { headers: headers(token) }) return unwrap(res, 'Could not list your Supabase projects') } /** Plan of one organization, which the list endpoint does not carry. */ export async function getSupabaseOrgPlan(token: string, slug: string): Promise { try { - const res = await fetch(`${base}/api/oauth/get_supabase_org/${slug}`, { + const res = await fetch(`/api/oauth/get_supabase_org/${slug}`, { headers: headers(token) }) if (!res.ok) return undefined @@ -132,7 +131,7 @@ export async function createSupabaseProject( token: string, args: { name: string; organizationSlug: string; region: string; dbPass: string } ): Promise { - const res = await fetch(`${base}/api/oauth/create_supabase_project`, { + const res = await fetch('/api/oauth/create_supabase_project', { method: 'POST', headers: headers(token), body: JSON.stringify({ @@ -185,7 +184,7 @@ export async function waitUntilSupabaseHealthy( * that landed on another one, and the resulting resource never connects. */ export async function getSupabasePooler(token: string, projectId: string): Promise { - const res = await fetch(`${base}/api/oauth/get_supabase_pooler/${projectId}`, { + const res = await fetch(`/api/oauth/get_supabase_pooler/${projectId}`, { headers: headers(token) }) const configs: SupabasePooler[] = await unwrap(res, 'Could not read the connection details') diff --git a/frontend/src/lib/draftAddRedirect.ts b/frontend/src/lib/draftAddRedirect.ts index 242893f782..2c027ac1da 100644 --- a/frontend/src/lib/draftAddRedirect.ts +++ b/frontend/src/lib/draftAddRedirect.ts @@ -1,5 +1,4 @@ import { redirect } from '@sveltejs/kit' -import { base } from '$app/paths' import { mintDraftPath } from '$lib/mintDraftPath' /** @@ -8,7 +7,7 @@ import { mintDraftPath } from '$lib/mintDraftPath' * * Mints a fresh `u//draft_` path (via the shared `mintDraftPath`, * so the SDK builder wrappers stay in lockstep) and 307s to - * `{base}/{editPrefix}/?new_draft=true&`. `new_draft=true` + * `/{editPrefix}/?new_draft=true&`. `new_draft=true` * tells the edit route to seed an empty editor instead of 404-ing. The hash is * carried over too — fork / handler-template buttons encode a payload into it * that the edit route's `new_draft` branch consumes. @@ -23,6 +22,6 @@ export function makeDraftAddLoad(editPrefix: string) { // points at the PREVIOUS page, but every hash-payload producer arrives // as a full page load, so the hash is correct. const hash = typeof window !== 'undefined' ? window.location.hash : '' - redirect(307, `${base}/${editPrefix}/${path}?${params.toString()}${hash}`) + redirect(307, `/${editPrefix}/${path}?${params.toString()}${hash}`) } } diff --git a/frontend/src/lib/githubApp.ts b/frontend/src/lib/githubApp.ts index 666734546e..0c19cd8de2 100644 --- a/frontend/src/lib/githubApp.ts +++ b/frontend/src/lib/githubApp.ts @@ -4,7 +4,6 @@ import { type GetGhesConfigResponse } from '$lib/gen' import { sendUserToast } from '$lib/toast' -import { base } from '$lib/base' export interface GitHubAppState { loadingGithubInstallations: boolean @@ -137,7 +136,7 @@ export async function loadGithubInstallations( const stateParam = encodeURIComponent( JSON.stringify({ workspace_id: currentWorkspace, - base_url: window.location.origin + base + base_url: window.location.origin }) ) diff --git a/frontend/src/lib/navigation.test.ts b/frontend/src/lib/navigation.test.ts index f499ddf1f7..d7f503b4eb 100644 --- a/frontend/src/lib/navigation.test.ts +++ b/frontend/src/lib/navigation.test.ts @@ -2,11 +2,10 @@ import { describe, it, expect, vi } from 'vitest' const goto = vi.hoisted(() => vi.fn(async (_path: string) => {})) vi.mock('$app/navigation', () => ({ goto: (path: string) => goto(path) })) -vi.mock('$app/paths', () => ({ base: '' })) import { buildFilterUrl, setQuery } from './navigation' -// Parse the (un-based) app path the builder returns against a dummy origin so we can +// Parse the app path the builder returns against a dummy origin so we can // assert on pathname / searchParams / hash without caring about ordering. function parse(appPath: string): URL { return new URL(appPath, 'http://x') diff --git a/frontend/src/lib/navigation.ts b/frontend/src/lib/navigation.ts index db8b9f3a80..164b9402c1 100644 --- a/frontend/src/lib/navigation.ts +++ b/frontend/src/lib/navigation.ts @@ -1,23 +1,14 @@ -import { goto as svelteGoto } from '$app/navigation' -import { base as svelteBase } from '$app/paths' +import { goto } from '$app/navigation' import { serializeParam } from '$lib/utils/serializeParam' -export function goto(path: string, options = {}) { - if (svelteBase == '' || path.startsWith('?')) { - return svelteGoto(path, options) - } else { - const fullPath = path.startsWith(svelteBase) ? path : `${svelteBase}${path}` - return svelteGoto(fullPath, options) - } -} +export { goto } /** * Build an in-app deep-link to `pathname` with query-param filters, encoded exactly * as the pages write them (via `serializeParam`) so `useUrlSyncedFilterInstance` * round-trips them back into filter state. Nullish/empty values are dropped; when * `validKeys` is provided, unknown keys are dropped too (structured output guarantees - * shape, not truth). Returns an un-prefixed app path — pass it to `goto`, which adds - * the SvelteKit base. + * shape, not truth). Returns an app path to pass to `goto`. */ export function buildFilterUrl( pathname: string, diff --git a/frontend/src/lib/remoteDeploy.ts b/frontend/src/lib/remoteDeploy.ts index ad887504fb..dbac372f94 100644 --- a/frontend/src/lib/remoteDeploy.ts +++ b/frontend/src/lib/remoteDeploy.ts @@ -1,4 +1,3 @@ -import { base } from '$lib/base' import type { RemoteDeployConnection, RemoteDeployTarget } from '$lib/gen' import { randomSecret } from '$lib/utils/uuid' @@ -67,7 +66,7 @@ export function remoteDeployAuthorizeUrl( localStorage.setItem(PENDING_PREFIX + state, JSON.stringify(pending)) const params = new URLSearchParams({ workspace: target.workspace_id, - callback: `${window.location.origin}${base}${CALLBACK_PATH}`, + callback: `${window.location.origin}${CALLBACK_PATH}`, state }) return `${target.base_url}/user/remote_deploy_authorize?${params}` diff --git a/frontend/src/lib/scripts.ts b/frontend/src/lib/scripts.ts index 6538db29e7..f9f1fa775e 100644 --- a/frontend/src/lib/scripts.ts +++ b/frontend/src/lib/scripts.ts @@ -1,5 +1,4 @@ import { get } from 'svelte/store' -import { base } from '$lib/base' import type { Schema, SupportedLanguage } from './common' import { FlowService, type Script, ScriptService, ScheduleService } from './gen' import { hubBaseUrlStore, workspaceStore } from './stores' @@ -136,7 +135,7 @@ export function scriptPathToHref(path: string, hubBaseUrl: string): string { if (path.startsWith('hub/')) { return hubBaseUrl + '/from_version/' + path.substring(4) } else { - return `${base}/scripts/get/${path}?workspace=${get(workspaceStore)}` + return `/scripts/get/${path}?workspace=${get(workspaceStore)}` } } @@ -146,7 +145,7 @@ export function flowPathToHref(path: string, hubBaseUrl: string = get(hubBaseUrl return hubFlowId ? `${hubBaseUrl}/flows/${hubFlowId}` : hubBaseUrl } - return `${base}/flows/get/${path}?workspace=${get(workspaceStore)}` + return `/flows/get/${path}?workspace=${get(workspaceStore)}` } const scriptLanguagesArray: [SupportedLanguage | 'docker' | 'bunnative', string][] = [ diff --git a/frontend/src/lib/utils/editInFork.ts b/frontend/src/lib/utils/editInFork.ts index 67eebcf7eb..d43c3644c9 100644 --- a/frontend/src/lib/utils/editInFork.ts +++ b/frontend/src/lib/utils/editInFork.ts @@ -1,4 +1,3 @@ -import { base } from '$lib/base' import { get } from 'svelte/store' import { userStore, @@ -75,13 +74,13 @@ export function canCreateFork(user: UserExt | undefined): boolean { function editPathFor(itemType: ItemType, itemPath: string): string { switch (itemType) { case 'script': - return `${base}/scripts/edit/${itemPath}` + return `/scripts/edit/${itemPath}` case 'flow': - return `${base}/flows/edit/${itemPath}` + return `/flows/edit/${itemPath}` case 'app': - return `${base}/apps/edit/${itemPath}` + return `/apps/edit/${itemPath}` case 'raw_app': - return `${base}/apps_raw/edit/${itemPath}` + return `/apps_raw/edit/${itemPath}` } } @@ -101,7 +100,7 @@ export function buildForkEditUrl( * workspace the page loads in, so a caller acting on another one names it. */ export function forkWorkspaceUrl(itemType: ItemType, itemPath: string, workspace?: string): string { const from = workspace ? `&workspace=${encodeURIComponent(workspace)}` : '' - return `${base}/user/fork_workspace?rd=${encodeURIComponent(editPathFor(itemType, itemPath))}${from}` + return `/user/fork_workspace?rd=${encodeURIComponent(editPathFor(itemType, itemPath))}${from}` } /** The item's editor in the dev workspace — the target `buildForkEditUrl` produces when a dev exists. */ diff --git a/frontend/src/lib/workspaceCreation.ts b/frontend/src/lib/workspaceCreation.ts index 34c1644462..0a31d66a96 100644 --- a/frontend/src/lib/workspaceCreation.ts +++ b/frontend/src/lib/workspaceCreation.ts @@ -13,7 +13,6 @@ import { SettingService, UserService, WorkspaceService } from '$lib/gen' import { usersWorkspaceStore } from '$lib/stores' import { switchWorkspace } from '$lib/storeUtils' import { isCloudHosted } from '$lib/cloud' -import { base } from '$lib/base' import { WORKSPACE_NAME_MAX_LENGTH } from '$lib/utils/workspaceId' /** @@ -30,7 +29,7 @@ import { WORKSPACE_NAME_MAX_LENGTH } from '$lib/utils/workspaceId' export async function canCreateWorkspace(isSuperadmin: boolean): Promise { if (isSuperadmin || isCloudHosted()) return true try { - const r = await fetch(base + '/api/workspaces/create_workspace_require_superadmin') + const r = await fetch('/api/workspaces/create_workspace_require_superadmin') if ((await r.text()) != 'true') return true return !!(await UserService.globalWhoami()).super_admin } catch { diff --git a/frontend/src/routes/(root)/(logged)/+layout.svelte b/frontend/src/routes/(root)/(logged)/+layout.svelte index 10a4d2d0ef..5edf490220 100644 --- a/frontend/src/routes/(root)/(logged)/+layout.svelte +++ b/frontend/src/routes/(root)/(logged)/+layout.svelte @@ -102,7 +102,6 @@ import InstanceUiSync from '$lib/components/InstanceUiSync.svelte' import { onDestroy, setContext, untrack } from 'svelte' import { cubicOut } from 'svelte/easing' - import { base } from '$app/paths' import { Menubar, Tooltip } from '$lib/components/meltComponents' import { aiChatManager } from '$lib/components/copilot/chat/AIChatManager.svelte' import AiChatLayout from '$lib/components/copilot/chat/AiChatLayout.svelte' @@ -113,7 +112,7 @@ import { isPageItemListPath, parsePreviewItemRoute, - stripBase + barePath } from '$lib/components/sessions/previewPaths' import { rememberNavRoute } from '$lib/components/sessions/sessionSwitch.svelte' import { sessionState } from '$lib/components/sessions/sessionState.svelte' @@ -303,7 +302,7 @@ // An operator without the opt-in (`gate.ts`) is refused by /sessions, so their sidebar is not // handed over to a session list the page behind it declines, and they keep the docked chat. const sessionsAllowed = $derived(sessionsAllowedFor($userStore?.operator)) - let sessionMode = $derived(page.url.pathname.startsWith(base + '/sessions') && sessionsAllowed) + let sessionMode = $derived(page.url.pathname.startsWith('/sessions') && sessionsAllowed) // Session mode points the bottom settings entry at the open session's own // workspace. An unsent draft hasn't committed one yet, so it falls back to // the family root. @@ -468,7 +467,7 @@ } // A list page mounts in process too, where its rows open as tabs of their own. const listUrl = navigation.to?.url - if (listUrl && isPageItemListPath(stripBase(listUrl.pathname))) { + if (listUrl && isPageItemListPath(barePath(listUrl.pathname))) { navigation.cancel() const u = new URL(listUrl.href) u.searchParams.delete('nomenubar') @@ -719,7 +718,7 @@ // Remember the last navigation-mode route so exiting session mode returns // the user where they were rather than to the home page. const to = n.to?.url - if (to && !to.pathname.startsWith(base + '/sessions')) { + if (to && !to.pathname.startsWith('/sessions')) { rememberNavRoute(to.pathname + to.search) } }) @@ -734,7 +733,7 @@ } } - let devOnly = $derived(page.url.pathname.startsWith(base + '/scripts/dev')) + let devOnly = $derived(page.url.pathname.startsWith('/scripts/dev')) async function loadDefaultScripts(workspace: string, user: UserExt | undefined) { if (!user?.operator) { @@ -1051,7 +1050,7 @@

diff --git a/frontend/src/routes/(root)/(logged)/agents/get/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/agents/get/[...path]/+page.svelte index 3070345ddf..ceb37c0a10 100644 --- a/frontend/src/routes/(root)/(logged)/agents/get/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/agents/get/[...path]/+page.svelte @@ -2,7 +2,6 @@ import { page } from '$app/state' import { FlaskConical, FormInput, MessageSquare, Pen } from 'lucide-svelte' import { resource } from 'runed' - import { base } from '$lib/base' import { goto } from '$lib/navigation' import { copilotInfo } from '$lib/aiStore' import AgentEvalsModal from '$lib/components/flows/content/AgentEvalsModal.svelte' @@ -60,7 +59,7 @@ } async function remove() { - if (ws && (await deleteAgent(ws, path))) await goto(`${base}/?kind=agent`) + if (ws && (await deleteAgent(ws, path))) await goto('/?kind=agent') } // The workspace's, so a path change never waits on it and a menu never outlives its path. @@ -122,7 +121,7 @@ ? async (newPath) => { // A rename moves the page; a summary saved in place has to be read again, since // what is shown here came from the load this page keys on. - if (newPath !== path) await goto(`${base}/agents/get/${newPath}`) + if (newPath !== path) await goto(`/agents/get/${newPath}`) else reloaded++ } : undefined} @@ -146,7 +145,7 @@ variant: 'accent', unifiedSize: 'md', startIcon: Pen, - href: `${base}/agents/edit/${path}` + href: `/agents/edit/${path}` } } ] diff --git a/frontend/src/routes/(root)/(logged)/apps/get_raw/[version]/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/apps/get_raw/[version]/[...path]/+page.svelte index dbba9ce790..edc0c2d918 100644 --- a/frontend/src/routes/(root)/(logged)/apps/get_raw/[version]/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/apps/get_raw/[version]/[...path]/+page.svelte @@ -1,6 +1,5 @@ diff --git a/frontend/src/routes/(root)/(logged)/concurrency_groups/+page.svelte b/frontend/src/routes/(root)/(logged)/concurrency_groups/+page.svelte index 2272449e0f..9e58c633e7 100644 --- a/frontend/src/routes/(root)/(logged)/concurrency_groups/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/concurrency_groups/+page.svelte @@ -1,7 +1,6 @@ {#if workspaceSettingsInitialized === false} - {#if fromWorkspaceSettings} - -
-

Double check the S3 resource fields and try again.

-
-
- {:else if s3ResourcePath} - -
-

- Double check the resource fields and that its object storage is reachable, then try again. -

-
-
- {:else} - -
- - {/if} +
+ {#if fromWorkspaceSettings} + +
+

Double check the S3 resource fields and try again.

+
+
+ {:else if s3ResourcePath} + +
+

+ Double check the resource fields and that its object storage is reachable, then try + again. +

+
+
+ {:else} + +
+

+ The workspace needs to be connected to an S3 storage to use this feature. You can configure it here. +

+
+
+ {/if} +
{:else} {#if fileListUnavailable == true} {#if replaceUnauthorizedWarning} {@render replaceUnauthorizedWarning()} {:else} -
+

You don't have access to the S3 bucket resource and your administrator has restricted @@ -1097,28 +1214,25 @@

{/if} {/if} -
+ {#if !fileListUnavailable} -
+ {#if !rootPath} -
- +
{/if} {#if displayedFileKeys.length === 0} {#if fileListLoading} -
-
- Loading content -
+
+ + Loading...
{:else} -
+
{#if filter.trim() !== ''} No files starting with "{filter.trim()}" {:else} @@ -1130,19 +1244,19 @@ can remove every match. Without this the only control that could resume the listing would be hidden behind the empty state. -->
-
{/if} {/if} {:else} -
+
{#snippet header()}{/snippet} {#snippet footer()}{/snippet} @@ -1154,199 +1268,249 @@ {@const load_more_prefix = is_load_more ? item_key.slice(0, -1) : ''} {@const file_info = allFilesByKey[item_key]} -
+
{#if is_load_more} {@const loadMoreNesting = nestingLevelOf(load_more_prefix + 'x') - 2 * rootPathNestingLevel} {@const loadingMore = folderState[load_more_prefix]?.loading === true} - - -
!loadingMore && loadMore(load_more_prefix)} - class={twMerge( - 'flex flex-row h-full text-xs items-center justify-start text-secondary', - loadingMore ? 'cursor-default' : 'cursor-pointer' - )} + disabled={loadingMore} + class="w-full text-xs font-normal flex gap-2 items-center h-8 pr-1 text-secondary hover:bg-surface-hover cursor-pointer disabled:cursor-default disabled:hover:bg-transparent" + style={treeRowIndent(loadMoreNesting)} > -
- -
- {#if loadingMore} - - {:else} - - {/if} -
-
Load more
-
-
+ + {#if loadingMore} + + {:else} + + {/if} + Load more + {:else if file_info} {@const nestingLevel = file_info.nestingLevel - 2 * rootPathNestingLevel} - - -
selectItem(index)} + title={file_info.full_key.slice(rootPath.length)} class={twMerge( - 'flex flex-row h-full font-semibold text-xs items-center justify-start', - selectedFileKey !== undefined && selectedFileKey.s3 === file_info.full_key - ? 'bg-surface-hover' - : '' + 'w-full text-xs font-normal text-primary flex gap-2 items-center h-8 pr-1 cursor-pointer', + isSelected ? 'bg-surface-secondary' : 'hover:bg-surface-hover' )} + style={treeRowIndent(nestingLevel)} > -
- {#if file_info.type === 'folder'} - {#if folderState[file_info.full_key]?.loading} - - {:else if file_info.collapsed}{:else}{/if} -
- - {#if file_info.display_name === ''} - (empty name) - {:else}{file_info.display_name}{/if} - {#if !lazyMode} - ({file_info.count}{count % 1000 === 0 && - lastKeyFolders[file_info.nestingLevel / 2] === file_info.display_name - ? '+' - : ''} item{file_info.count === 1 ? '' : 's'}) - {/if} -
+ {#if isFolder} + {#if folderState[file_info.full_key]?.loading} + {:else} - -
- {file_info.display_name} -
+ {/if} -
-
+ {:else} + + {/if} + + + + {#if file_info.display_name === ''} + (empty name) + {:else}{file_info.display_name}{/if} + + + {#if isFolder && !lazyMode} + + {file_info.count}{count % 1000 === 0 && + lastKeyFolders[file_info.nestingLevel / 2] === file_info.display_name + ? '+' + : ''} + + {/if} + {/if}
{/snippet}
{#if fileListLoading === true} -
- Loading content +
+ Loading...
{:else if lazyMode}
{displayedCount} item{displayedCount === 1 ? '' : 's'} shown
{:else} -
+
{displayedCount}{flatHasMore ? '+' : ''} {displayedCount !== count ? 'filtered ' : ''}items (including inside folders)
{#if flatHasMore} - {/if} {/if}
{/if} -
+ {/if} -
- {#if fileMetadata === undefined} -
- {#if fileInfoLoading} -
- {:else if fileListUnavailable} -
- {:else} -
- {/if} -
- {:else} -
-
-

- {((p) => (p.startsWith(rootPath) ? p.slice(rootPath.length) : p))( - fileMetadata.fileKey - )} -

- {#if filePreview !== undefined && (!hideS3SpecificDetails || !readOnlyMode || allowDelete)} -
- {#if !hideS3SpecificDetails} - {@const downloadApiPath = `/w/${ws}/job_helpers/download_s3_file?file_key=${encodeURIComponent(fileMetadata?.fileKey ?? '')}${storage ? `&storage=${storage}` : ''}${s3ResourcePath ? `&s3_resource_path=${encodeURIComponent(s3ResourcePath)}` : ''}`} - {@const downloadName = - fileMetadata?.fileKey.split('/').pop() ?? 'unnamed_download.file'} - {#if shouldDownloadViaClient()} -
+ {:else if !csvOptions} +
+ {#if filePreview.contentType === 'Csv'} + Previewing a {filePreview.contentType?.toLowerCase()} file. Separator character: +
+ +
+ Header row: +
+ +
+ {:else} + Previewing a {filePreview.contentType?.toLowerCase()} file. + {/if} +
+ {/if}
{#if !emptyString(filePreview.contentPreview)}{filePreview.contentPreview}{:else if filePreview.contentType !== undefined}Preview impossible.{/if}
diff --git a/frontend/src/lib/components/apps/components/display/dbtable/utils.ts b/frontend/src/lib/components/apps/components/display/dbtable/utils.ts index 21371aa08b..ae3a82e97c 100644 --- a/frontend/src/lib/components/apps/components/display/dbtable/utils.ts +++ b/frontend/src/lib/components/apps/components/display/dbtable/utils.ts @@ -41,6 +41,8 @@ export type ColumnDef = { children?: ColumnDef[] // DBExplorer ignored?: boolean + /** False when the column cannot be filtered on, e.g. a nested value. */ + filterable?: boolean hideInsert?: boolean editable?: boolean overrideDefaultValue?: boolean diff --git a/frontend/src/lib/components/dbTableFilters.ts b/frontend/src/lib/components/dbTableFilters.ts index 9d9c83e6b0..c3266b3ac7 100644 --- a/frontend/src/lib/components/dbTableFilters.ts +++ b/frontend/src/lib/components/dbTableFilters.ts @@ -52,7 +52,7 @@ export function buildDbTableFilterSchema(columns: ColumnDef[]): DbTableFilterSch const columnOfKey: Record = {} const keyOfColumn: Record = {} for (const col of columns) { - if (!col.field) continue + if (!col.field || col.filterable === false) continue let base = col.field.replace(/\W+/g, '_') || 'column' if (base === FREE_TEXT_KEY) base = `${base}col` let key = base diff --git a/frontend/src/lib/components/s3TableOps.test.ts b/frontend/src/lib/components/s3TableOps.test.ts new file mode 100644 index 0000000000..f32ce47c56 --- /dev/null +++ b/frontend/src/lib/components/s3TableOps.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest' +import { datatypeOf, jsonTableRows, serverTableFormat } from './s3TableOps' +import { dbColumnKind } from './dbTableFilters' + +describe('serverTableFormat', () => { + it('pages delimited text and newline-delimited JSON through the csv reader', () => { + expect(serverTableFormat('a/b.parquet')).toBe('parquet') + for (const key of ['a.csv', 'a.TSV', 'a.jsonl', 'a.ndjson']) { + expect(serverTableFormat(key)).toBe('csv') + } + }) + + it('leaves plain JSON to the browser', () => { + expect(serverTableFormat('a.json')).toBeUndefined() + expect(serverTableFormat('a.txt')).toBeUndefined() + }) +}) + +describe('datatypeOf', () => { + it('maps Arrow types to the kinds the grid filters by', () => { + expect(dbColumnKind(datatypeOf('Int64'))).toBe('number') + expect(dbColumnKind(datatypeOf('UInt8'))).toBe('number') + expect(dbColumnKind(datatypeOf('Float64'))).toBe('number') + expect(dbColumnKind(datatypeOf('Decimal128(21, 1)'))).toBe('number') + expect(dbColumnKind(datatypeOf('Boolean'))).toBe('boolean') + expect(dbColumnKind(datatypeOf('Utf8View'))).toBe('text') + }) + + it('names a nested type without its field list', () => { + expect(datatypeOf('List(Field { name: "item", data_type: Utf8 })')).toBe('list') + expect(datatypeOf('Timestamp(Microsecond, None)')).toBe('timestamp') + }) +}) + +describe('jsonTableRows', () => { + it('keeps objects and wraps other elements in a value column', () => { + expect(jsonTableRows([{ a: 1 }, 2, null])).toEqual([{ a: 1 }, { value: 2 }, { value: null }]) + }) + + it('has no table for an empty array or a non-array', () => { + expect(jsonTableRows([])).toBeUndefined() + expect(jsonTableRows({ a: 1 })).toBeUndefined() + }) +}) diff --git a/frontend/src/lib/components/s3TableOps.ts b/frontend/src/lib/components/s3TableOps.ts new file mode 100644 index 0000000000..f5d7bd758b --- /dev/null +++ b/frontend/src/lib/components/s3TableOps.ts @@ -0,0 +1,170 @@ +import { HelpersService, OpenAPI } from '$lib/gen' +import { getHeaders } from '$lib/gen/core/request' +import { ColumnIdentity, type ColumnDef } from './apps/components/display/dbtable/utils' +import type { IDbTableOps } from './dbOps' + +/** Files the server can page through as a table, without reading them whole. */ +export function serverTableFormat(fileKey: string): 'parquet' | 'csv' | undefined { + const key = fileKey.toLowerCase() + if (key.endsWith('.parquet')) return 'parquet' + if (/\.(csv|tsv|jsonl|ndjson)$/.test(key)) return 'csv' + return undefined +} + +export type S3TableSource = { + workspace: string + fileKey: string + storage?: string + s3ResourcePath?: string + csvSeparator?: string + csvHasHeader?: boolean +} + +type Chunk = { + columns: string[] + column_types?: string[] + rows: Record[] + csv_separator?: string +} + +/** An Arrow type as the SQL type name the grid filters and formats by. */ +export function datatypeOf(arrowType: string | undefined): string { + const t = arrowType ?? '' + if (/^U?Int\d+$/.test(t)) return 'bigint' + if (/^Float\d+$/.test(t)) return 'double' + if (/^Decimal/.test(t)) return 'decimal' + if (t === 'Boolean') return 'boolean' + if (/^(Large)?Utf8/.test(t)) return 'text' + // A nested type spells out its fields, e.g. `List(Field { name: "item", … })`. + return t.split('(')[0].toLowerCase() || 'text' +} + +/** Read-only table ops over an object storage file, paged, searched, filtered and sorted + * by the server, with the CSV separator it read the file with (guessed when none is given). + */ +export async function s3TableOps( + source: S3TableSource +): Promise<{ ops: IDbTableOps; csvSeparator?: string }> { + const format = serverTableFormat(source.fileKey) + if (!format) throw new Error(`${source.fileKey} cannot be read as a table`) + const common = { + workspace: source.workspace, + path: source.fileKey, + storage: source.storage, + s3ResourcePath: source.s3ResourcePath + } + const csvOptions: { csvSeparator?: string; csvHasHeader?: boolean } = + format === 'csv' ? { csvSeparator: source.csvSeparator, csvHasHeader: source.csvHasHeader } : {} + const filtersParam = (columnFilters: Record | undefined) => + columnFilters && Object.keys(columnFilters).length ? JSON.stringify(columnFilters) : undefined + const searchParam = (quicksearch: string) => quicksearch.trim() || undefined + + async function loadChunk(params: { + offset: number + limit: number + quicksearch?: string + columnFilters?: Record + orderBy?: string + isDesc?: boolean + }): Promise { + const data = { + ...common, + ...csvOptions, + offset: params.offset, + limit: params.limit, + searchTerm: searchParam(params.quicksearch ?? ''), + filters: filtersParam(params.columnFilters), + sortCol: params.orderBy, + sortDesc: params.isDesc + } + return (await (format === 'parquet' + ? HelpersService.loadParquetPreview(data) + : HelpersService.loadCsvPreview(data))) as Chunk + } + + const head = await loadChunk({ offset: 0, limit: 0 }) + // Later reads reuse the separator the server guessed, rather than each guessing again. + if (format === 'csv') csvOptions.csvSeparator ??= head.csv_separator + const colDefs: ColumnDef[] = head.columns.map((field, i) => ({ + field, + datatype: datatypeOf(head.column_types?.[i]), + // The server cannot read a struct, map or union as text to filter it. + filterable: !/^(Struct|Map|Union)/.test(head.column_types?.[i] ?? ""), + defaultvalue: '', + isprimarykey: false, + isidentity: ColumnIdentity.No, + isnullable: 'YES', + isenum: false + })) + + const ops: IDbTableOps = { + dbType: 'duckdb', + tableKey: source.fileKey.split('/').pop() ?? source.fileKey, + colDefs, + getCount: async ({ quicksearch, columnFilters }) => { + const res = await HelpersService.loadTableRowCount({ + ...common, + ...csvOptions, + searchTerm: searchParam(quicksearch), + filters: filtersParam(columnFilters) + }) + return res.count ?? 0 + }, + getRows: async ({ + offset, + limit, + quicksearch, + columnFilters, + order_by, + is_desc, + explicitSort + }) => + ( + await loadChunk({ + offset, + limit, + quicksearch, + columnFilters, + // Unsorted, a file keeps its own row order. + orderBy: explicitSort ? order_by : undefined, + isDesc: is_desc + }) + ).rows + } + return { ops, csvSeparator: head.csv_separator } +} + +/** Plain JSON cannot be paged by the server, so only a file this small is read whole into + * the browser to be shown as a table. */ +export const JSON_TABLE_MAX_BYTES = 10 * 1024 * 1024 + +export function isJsonFile(fileKey: string): boolean { + return fileKey.toLowerCase().endsWith('.json') +} + +/** Rows of a JSON array, an element that is not an object becoming a one-column row; + * undefined for anything else. */ +export function jsonTableRows(value: unknown): Record[] | undefined { + if (!Array.isArray(value) || value.length === 0) return undefined + return value.map((v) => + v !== null && typeof v === 'object' && !Array.isArray(v) + ? (v as Record) + : { value: v } + ) +} + +export async function fetchJsonTableRows( + source: S3TableSource +): Promise[] | undefined> { + const query = new URLSearchParams({ file_key: source.fileKey }) + if (source.storage) query.set('storage', source.storage) + if (source.s3ResourcePath) query.set('s3_resource_path', source.s3ResourcePath) + const apiPath = `/w/${source.workspace}/job_helpers/download_s3_file?${query}` + const headers = await getHeaders(OpenAPI, { method: 'GET', url: apiPath }) + const response = await fetch(`${OpenAPI.BASE}${apiPath}`, { + headers, + credentials: OpenAPI.CREDENTIALS + }) + if (!response.ok) throw new Error(`${response.status} ${response.statusText}`) + return jsonTableRows(await response.json()) +} From 478eef735222e60de8ed6086f6b036e2346f1de4 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Mon, 12 Oct 2026 09:56:48 +0200 Subject: [PATCH 17/20] fix: resolve $var: references in mcp resource headers (#11679) * fix: resolve $var: references in mcp resource headers Co-Authored-By: Claude Opus 5.5 * fix: require variables:read scope to resolve mcp header variables Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- backend/tests/mcp_header_var_refs.rs | 110 ++++++++++++++++++++++++ backend/windmill-api/src/mcp_tools.rs | 28 +++--- backend/windmill-mcp/src/client/mod.rs | 3 +- backend/windmill-worker/src/ai/utils.rs | 14 ++- 4 files changed, 143 insertions(+), 12 deletions(-) create mode 100644 backend/tests/mcp_header_var_refs.rs diff --git a/backend/tests/mcp_header_var_refs.rs b/backend/tests/mcp_header_var_refs.rs new file mode 100644 index 0000000000..158bc71a5f --- /dev/null +++ b/backend/tests/mcp_header_var_refs.rs @@ -0,0 +1,110 @@ +//! An MCP resource header whose value is a `$var:` reference reaches the MCP +//! server as the variable's value, and only for a caller allowed to read it. +#![cfg(feature = "mcp")] + +use sqlx::{Pool, Postgres}; +use tokio::io::AsyncReadExt; +use windmill_common::client::AuthedClient; +use windmill_test_utils::*; +use windmill_worker::{load_mcp_tools, McpResourceConfig}; + +const SECRET_VALUE: &str = "S3CRET-MCP-HEADER-VALUE"; + +/// A secret variable and an MCP resource at `url` sending it as a header. +async fn seed(db: &Pool, url: &str) -> anyhow::Result<()> { + let mc = windmill_common::variables::build_crypt(db, "test-workspace").await?; + sqlx::query( + "INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms) + VALUES ('test-workspace', 'u/test-user/mcp_header', $1, true, '', '{}')", + ) + .bind(windmill_common::variables::encrypt(&mc, SECRET_VALUE)) + .execute(db) + .await?; + sqlx::query( + "INSERT INTO resource (workspace_id, path, value, resource_type, extra_perms, created_by) + VALUES ('test-workspace', 'u/test-user/mcp', $1, 'mcp', '{}', 'test-user')", + ) + .bind(serde_json::json!({ + "name": "mcp", + "url": url, + "headers": { "X-Api-Key": "$var:u/test-user/mcp_header", "X-Plain": "plain" } + })) + .execute(db) + .await?; + Ok(()) +} + +#[sqlx::test(fixtures("base"))] +async fn test_mcp_header_var_ref_is_resolved(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + std::env::set_var("ALLOW_PRIVATE_MCP_SERVER_URLS", "true"); + + // Not an MCP server: it captures the first request and hangs up, so loading + // fails at the connect step after the headers were sent. + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; + let mcp_port = listener.local_addr()?.port(); + let first_request = tokio::spawn(async move { + let (mut socket, _) = listener.accept().await?; + let mut buf = vec![0u8; 16 * 1024]; + let n = socket.read(&mut buf).await?; + anyhow::Ok(String::from_utf8_lossy(&buf[..n]).to_lowercase()) + }); + seed(&db, &format!("http://127.0.0.1:{mcp_port}/mcp")).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 config = vec![McpResourceConfig { + resource_path: "$res:u/test-user/mcp".to_string(), + include_tools: None, + exclude_tools: None, + }]; + let _ = load_mcp_tools(&db, "test-workspace", config, &client).await; + + let request = first_request.await??; + assert!( + request.contains(&format!("x-api-key: {}", SECRET_VALUE.to_lowercase())), + "the header must carry the variable's value, not the reference: {request}" + ); + assert!(request.contains("x-plain: plain"), "{request}"); + + Ok(()) +} + +/// A token scoped to the resource alone must not have the API read the +/// variable a header points at. +#[sqlx::test(fixtures("base"))] +async fn test_mcp_header_var_ref_requires_variable_scope(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + seed(&db, "https://mcp.invalid.windmill.test").await?; + sqlx::query( + "INSERT INTO token (token_hash, token_prefix, token, email, label, super_admin, scopes) + VALUES (encode(sha256('RES_ONLY_TOKEN'::bytea), 'hex'), 'RES_ONLY_T', 'RES_ONLY_TOKEN', + 'test@windmill.dev', 'resources only', false, '{resources:read}')", + ) + .execute(&db) + .await?; + + let server = ApiServer::start(db.clone()).await?; + let resp = reqwest::Client::new() + .get(format!( + "http://localhost:{}/api/w/test-workspace/resources/mcp_tools/u/test-user/mcp", + server.addr.port() + )) + .header("Authorization", "Bearer RES_ONLY_TOKEN") + .send() + .await?; + let status = resp.status(); + let body = resp.text().await?; + assert_eq!(status, reqwest::StatusCode::FORBIDDEN, "{body}"); + assert!( + body.contains("variables:read:u/test-user/mcp_header"), + "{body}" + ); + + Ok(()) +} diff --git a/backend/windmill-api/src/mcp_tools.rs b/backend/windmill-api/src/mcp_tools.rs index e1931873a9..1e07f374de 100644 --- a/backend/windmill-api/src/mcp_tools.rs +++ b/backend/windmill-api/src/mcp_tools.rs @@ -23,14 +23,12 @@ async fn with_deadline( what: &str, fut: impl std::future::Future>, ) -> Result { - tokio::time::timeout(MCP_DEADLINE, fut) - .await - .map_err(|_| { - Error::ExecutionErr(format!( - "MCP server did not answer within {}s ({what})", - MCP_DEADLINE.as_secs() - )) - })? + tokio::time::timeout(MCP_DEADLINE, fut).await.map_err(|_| { + Error::ExecutionErr(format!( + "MCP server did not answer within {}s ({what})", + MCP_DEADLINE.as_secs() + )) + })? } /// Connect to the MCP server described by the `mcp` resource at `path`. @@ -65,8 +63,9 @@ async fn connect_mcp_client( let resource_value = not_found_if_none(resource_value_o, "Resource", path)? .ok_or_else(|| Error::BadRequest(format!("Empty resource value for {}", path)))?; - let mcp_resource = serde_json::from_str::(resource_value.0.get()) - .map_err(|e| Error::BadRequest(format!("Failed to parse MCP resource: {}", e)))?; + let mut mcp_resource = + serde_json::from_str::(resource_value.0.get()) + .map_err(|e| Error::BadRequest(format!("Failed to parse MCP resource: {}", e)))?; #[cfg(feature = "oauth2")] { @@ -126,6 +125,15 @@ async fn connect_mcp_client( None }; + // Header secrets go through the same permissioned path as the token. + for value in mcp_resource.headers.iter_mut().flatten().map(|(_, v)| v) { + if let Some(var_path) = value.strip_prefix("$var:") { + check_scopes(authed, || format!("variables:read:{}", var_path))?; + let db_authed = DbWithOptAuthed::from_authed(authed, db.clone(), Some(user_db.clone())); + *value = get_value_internal(&db_authed, w_id, var_path, false).await?; + } + } + windmill_mcp::McpClient::from_resource(mcp_resource, token) .await .map_err(|e| Error::ExecutionErr(format!("Failed to connect to MCP server: {}", e))) diff --git a/backend/windmill-mcp/src/client/mod.rs b/backend/windmill-mcp/src/client/mod.rs index 863d6b0355..5d13a40314 100644 --- a/backend/windmill-mcp/src/client/mod.rs +++ b/backend/windmill-mcp/src/client/mod.rs @@ -70,7 +70,8 @@ impl McpClient { headers.insert(name, value); } _ => { - tracing::warn!("Invalid header: {}={}", key, value); + // The value may be a resolved secret: keep it out of the logs. + tracing::warn!("Invalid header: {}", key); } } } diff --git a/backend/windmill-worker/src/ai/utils.rs b/backend/windmill-worker/src/ai/utils.rs index 7437184385..50bbcad58f 100644 --- a/backend/windmill-worker/src/ai/utils.rs +++ b/backend/windmill-worker/src/ai/utils.rs @@ -570,7 +570,7 @@ pub async fn load_mcp_tools( // is not allowed to read (resources:read:{path}). Reading through the raw // db pool here would bypass the authorization enforced by the regular MCP // tools API (get_mcp_tools) and act as a confused deputy. - let mcp_resource = client + let mut mcp_resource = client .get_resource_value::(path) .await .map_err(|e| { @@ -615,6 +615,18 @@ pub async fn load_mcp_tools( None }; + // Header secrets go through the same permissioned path as the token. + for (name, value) in mcp_resource.headers.iter_mut().flatten() { + if let Some(var_path) = value.strip_prefix("$var:") { + *value = client.get_variable_value(var_path).await.map_err(|e| { + Error::internal_err(format!( + "Failed to resolve variable {} for header {} of MCP resource {}: {}", + var_path, name, resource_name, e + )) + })?; + } + } + // Create new MCP client for this execution tracing::debug!("Creating fresh MCP client for {}", resource_name); let mcp_conn = McpClient::from_resource(mcp_resource, token) From b347e792f85a110cc99ec13ab52459def8079195 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Mon, 12 Oct 2026 10:47:52 +0200 Subject: [PATCH 18/20] make the packaged components resolvable by consumers (#11682) * fix: make the packaged components resolvable by consumers The svelte-package output of @windmill-labs/components imported the chat SDK from outside the package root, pointed `$lib/utils` at a directory with no index, and shipped an `import.meta.glob` on the `$lib` alias. Bundle the chat SDK entry point into the package, repair the directory specifier, make the glob relative, and fail packaging on any import a consumer cannot resolve. Co-Authored-By: Claude Opus 5.5 * fix: pin the chat sdk output root and widen the packaging checks Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- frontend/scripts/fix-svelte-module-imports.js | 77 ++++++++++++++++++- .../src/lib/components/mcp/providerIcon.ts | 5 +- 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/frontend/scripts/fix-svelte-module-imports.js b/frontend/scripts/fix-svelte-module-imports.js index 9cec6265a0..fe4fde4959 100644 --- a/frontend/scripts/fix-svelte-module-imports.js +++ b/frontend/scripts/fix-svelte-module-imports.js @@ -16,9 +16,22 @@ // tarball. Copy the registry into the package and rewrite those imports // to point at it (mirrors how package-system-prompts.js bundles // `$system_prompts`). +// +// 3. Chat SDK: the `windmill-chat` alias (svelte.config.js) points at +// `chat-sdk/src/index.ts`, which escapes the package root the same way. +// Compile that entry point into the package and rewrite those imports. +// +// 4. `$lib/utils` from inside `utils/`: svelte-package turns it into `./`, +// which is the `utils/` directory (no index) rather than `utils.js`. +// Point a directory specifier with no index at its sibling module. +// +// A relative import that still escapes the package root, or an +// `import.meta.glob` left on an alias, fails the build: either one +// publishes fine and only breaks in the consumer's bundler. +import { execFileSync } from 'child_process' import { copyFileSync, existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs' import { fileURLToPath } from 'url' -import { dirname, join, relative, resolve } from 'path' +import { dirname, join, relative, resolve, sep } from 'path' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) @@ -29,8 +42,37 @@ const oauthConnectDest = join(packageDir, 'oauth_connect.json') copyFileSync(oauthConnectSrc, oauthConnectDest) -const IMPORT_SPECIFIER_RE = /(\bfrom\s*|\bimport\s*\(?\s*)(['"])(\.\.?\/[^'"]*\.(?:svelte|json))\2/g +const chatSdkSrc = resolve(__dirname, '..', '..', 'chat-sdk', 'src', 'index.ts') +const chatSdkDest = join(packageDir, 'chat-sdk') + +// File arguments make tsc ignore every tsconfig and emit only what the entry +// point reaches, which leaves out the React and AI SDK adapters. +execFileSync( + 'npx', + [ + 'tsc', + chatSdkSrc, + '--outDir', + chatSdkDest, + '--rootDir', + dirname(chatSdkSrc), + '--declaration', + '--target', + 'ES2021', + '--module', + 'ESNext', + '--moduleResolution', + 'bundler', + '--skipLibCheck' + ], + { stdio: 'inherit', cwd: resolve(__dirname, '..') } +) + +const IMPORT_SPECIFIER_RE = /(\bfrom\s*|\bimport\s*\(?\s*)(['"])(\.\.?(?:\/[^'"]*)?)\2/g const OAUTH_CONNECT_RE = /^(\.\.\/)+backend\/oauth_connect\.json$/ +const CHAT_SDK_RE = /^(\.\.\/)+chat-sdk\/src\/index\.ts$/ +const DIRECTORY_RE = /(^|\/)\.\.?$|\/$/ +const ALIASED_GLOB_RE = /import\.meta\.glob(?:<[^>]*>)?\(\s*\[?\s*['"]\$/ function* walk(dir) { for (const entry of readdirSync(dir)) { @@ -50,9 +92,13 @@ function relativeSpecifier(fromFile, toFile) { let rewrittenFiles = 0 let rewrittenImports = 0 +const unresolvable = [] for (const file of walk(packageDir)) { const content = readFileSync(file, 'utf-8') + if (ALIASED_GLOB_RE.test(content)) { + unresolvable.push(`${relative(packageDir, file)}: import.meta.glob on an alias`) + } let changed = false const updated = content.replace(IMPORT_SPECIFIER_RE, (match, prefix, quote, specifier) => { if (OAUTH_CONNECT_RE.test(specifier)) { @@ -60,10 +106,27 @@ for (const file of walk(packageDir)) { rewrittenImports++ return `${prefix}${quote}${relativeSpecifier(file, oauthConnectDest)}${quote}` } + if (CHAT_SDK_RE.test(specifier)) { + changed = true + rewrittenImports++ + return `${prefix}${quote}${relativeSpecifier(file, join(chatSdkDest, 'index.js'))}${quote}` + } + const target = resolve(dirname(file), specifier) + if (target !== packageDir && !target.startsWith(packageDir + sep)) { + unresolvable.push(`${relative(packageDir, file)}: ${specifier}`) + return match + } + if (DIRECTORY_RE.test(specifier)) { + if (!existsSync(join(target, 'index.js')) && existsSync(target + '.js')) { + changed = true + rewrittenImports++ + return `${prefix}${quote}${relativeSpecifier(file, target + '.js')}${quote}` + } + return match + } if (!specifier.endsWith('.svelte')) { return match } - const target = resolve(dirname(file), specifier) // A real `.svelte` component file: leave the import alone. if (existsSync(target)) { return match @@ -82,6 +145,14 @@ for (const file of walk(packageDir)) { } } +if (unresolvable.length > 0) { + console.error( + `fix-svelte-module-imports: ${unresolvable.length} import(s) a consumer cannot resolve:\n` + + unresolvable.join('\n') + ) + process.exit(1) +} + console.log( `fix-svelte-module-imports: rewrote ${rewrittenImports} import(s) in ${rewrittenFiles} file(s)` ) diff --git a/frontend/src/lib/components/mcp/providerIcon.ts b/frontend/src/lib/components/mcp/providerIcon.ts index ed8d059a57..a666b2b1c5 100644 --- a/frontend/src/lib/components/mcp/providerIcon.ts +++ b/frontend/src/lib/components/mcp/providerIcon.ts @@ -12,8 +12,11 @@ import { findMcpEntry, findMcpEntryByUrl } from './registry' * the chat does not otherwise reach that barrel. `import.meta.glob` gives the * filenames at build time and the module only when a match is actually used, so * a workspace with two connections downloads two icons. + * + * The pattern is relative because svelte-package rewrites `$lib` only in import + * specifiers: an aliased glob ships as-is and no consumer's bundler expands it. */ -const iconModules = import.meta.glob('$lib/components/icons/*.svelte') as Record< +const iconModules = import.meta.glob('../icons/*.svelte') as Record< string, () => Promise<{ default: Component }> > From a8e2f1f1ca28e3352aff8a450188e3ff75a9637c Mon Sep 17 00:00:00 2001 From: hugocasa Date: Mon, 12 Oct 2026 11:45:03 +0200 Subject: [PATCH 19/20] fix: keep ai agent schema property order stable across jobs (#11687) * fix: keep ai agent schema property order stable across jobs Co-Authored-By: Claude Opus 5.5 * fix: keep tool parameter order when filtering preset inputs Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- backend/windmill-ai/src/types.rs | 45 +++++++++++++++++++------ backend/windmill-worker/src/ai/utils.rs | 35 +++++++++++++++++-- 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/backend/windmill-ai/src/types.rs b/backend/windmill-ai/src/types.rs index e591e85159..59733e352c 100644 --- a/backend/windmill-ai/src/types.rs +++ b/backend/windmill-ai/src/types.rs @@ -1,3 +1,4 @@ +use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; use std::collections::HashMap; @@ -533,7 +534,7 @@ pub struct OpenAPISchema { #[serde(skip_serializing_if = "Option::is_none")] pub items: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub properties: Option>>, + pub properties: Option>>, #[serde(skip_serializing_if = "Option::is_none")] pub required: Option>, #[serde(skip_serializing_if = "Option::is_none", rename = "oneOf")] @@ -560,9 +561,9 @@ pub struct OpenAPISchema { #[serde(skip_serializing_if = "Option::is_none", rename = "$ref")] pub ref_path: Option, #[serde(skip_serializing_if = "Option::is_none", rename = "$defs")] - pub defs: Option>>, + pub defs: Option>>, #[serde(skip_serializing_if = "Option::is_none")] - pub definitions: Option>>, + pub definitions: Option>>, // Schema composition #[serde(skip_serializing_if = "Option::is_none", rename = "allOf")] @@ -829,7 +830,7 @@ impl OpenAPISchema { // Merge properties (other's properties are added if key doesn't exist) if let Some(ref other_props) = other.properties { - let props = self.properties.get_or_insert_with(HashMap::new); + let props = self.properties.get_or_insert_with(IndexMap::new); for (key, value) in other_props { props.entry(key.clone()).or_insert_with(|| value.clone()); } @@ -880,13 +881,13 @@ impl OpenAPISchema { // Merge $defs and definitions if let Some(ref other_defs) = other.defs { - let defs = self.defs.get_or_insert_with(HashMap::new); + let defs = self.defs.get_or_insert_with(IndexMap::new); for (key, value) in other_defs { defs.entry(key.clone()).or_insert_with(|| value.clone()); } } if let Some(ref other_definitions) = other.definitions { - let definitions = self.definitions.get_or_insert_with(HashMap::new); + let definitions = self.definitions.get_or_insert_with(IndexMap::new); for (key, value) in other_definitions { definitions .entry(key.clone()) @@ -981,7 +982,29 @@ pub struct S3ObjectWithType { #[cfg(test)] mod tests { use super::*; - use std::collections::HashMap; + + /// The schema is part of the prompt prefix the provider caches on, so its keys must + /// serialize in the same order on every request. + #[test] + fn strict_schema_keeps_the_defined_property_order() { + let keys = [ + "zeta", "alpha", "mid", "beta", "omega", "gamma", "delta", "kappa", + ]; + let props = keys + .iter() + .map(|k| format!(r#""{k}":{{"type":"string"}}"#)) + .collect::>() + .join(","); + let mut schema: OpenAPISchema = + serde_json::from_str(&format!(r#"{{"type":"object","properties":{{{props}}}}}"#)) + .unwrap(); + schema.make_strict(); + + let value = serde_json::to_value(&schema).unwrap(); + let serialized: Vec<&String> = value["properties"].as_object().unwrap().keys().collect(); + assert_eq!(serialized, keys); + assert_eq!(value["required"], serde_json::json!(keys)); + } /// Whichever provider served the request, `input_tokens` has to end up meaning the /// whole prompt. Leaving the Anthropic shape as reported under-states it by the @@ -1248,7 +1271,7 @@ mod tests { #[test] fn test_make_strict_defs() { let def_schema = object_schema(vec![("prop", string_schema())]); - let mut defs = HashMap::new(); + let mut defs = IndexMap::new(); defs.insert("MyType".to_string(), Box::new(def_schema)); let mut schema = OpenAPISchema { defs: Some(defs), ..Default::default() }; @@ -1274,7 +1297,7 @@ mod tests { #[test] fn test_make_strict_definitions() { let def_schema = object_schema(vec![("prop", string_schema())]); - let mut definitions = HashMap::new(); + let mut definitions = IndexMap::new(); definitions.insert("MyType".to_string(), Box::new(def_schema)); let mut schema = OpenAPISchema { definitions: Some(definitions), ..Default::default() }; @@ -1528,7 +1551,7 @@ mod tests { #[test] fn test_flatten_all_of_preserves_defs() { let def_schema = object_schema(vec![("field", string_schema())]); - let mut defs = HashMap::new(); + let mut defs = IndexMap::new(); defs.insert("MyType".to_string(), Box::new(def_schema)); let schema_with_defs = OpenAPISchema { defs: Some(defs), ..Default::default() }; @@ -1642,7 +1665,7 @@ mod tests { schema_url: Some("http://json-schema.org/draft-07/schema#".to_string()), ..Default::default() }; - let mut defs = HashMap::new(); + let mut defs = IndexMap::new(); defs.insert("MyType".to_string(), Box::new(def_schema)); let mut schema = OpenAPISchema { diff --git a/backend/windmill-worker/src/ai/utils.rs b/backend/windmill-worker/src/ai/utils.rs index 50bbcad58f..f0a0b5c20f 100644 --- a/backend/windmill-worker/src/ai/utils.rs +++ b/backend/windmill-worker/src/ai/utils.rs @@ -103,9 +103,9 @@ pub fn filter_schema_by_input_transforms( .get_mut("properties") .and_then(|p| p.as_object_mut()) { - for key in &keys_to_remove { - properties.remove(key); - } + // `remove` swaps the last entry into the freed slot, so removing in set order + // would leave the survivors in a different order on every run. + properties.retain(|key, _| !keys_to_remove.contains(key)); } // Also remove from required array @@ -748,3 +748,32 @@ pub fn any_tool_needs_previous_result(tools: &[Tool]) -> bool { false }) } + +#[cfg(test)] +mod tests { + use super::*; + + /// A tool's schema sits in the prompt prefix the provider caches on, so the + /// parameters left for the model must keep their order on every run. + #[test] + fn filtering_preset_inputs_keeps_the_remaining_property_order() { + let keys = ["a", "b", "c", "d", "e", "f", "g", "h"]; + let props = keys + .iter() + .map(|k| format!(r#""{k}":{{"type":"string"}}"#)) + .collect::>() + .join(","); + let schema = + RawValue::from_string(format!(r#"{{"type":"object","properties":{{{props}}}}}"#)) + .unwrap(); + let preset = ["a", "c", "f"] + .iter() + .map(|k| (k.to_string(), InputTransform::Static { value: to_raw_value(&1) })) + .collect(); + + let filtered = filter_schema_by_input_transforms(schema, &preset).unwrap(); + let value: serde_json::Value = serde_json::from_str(filtered.get()).unwrap(); + let remaining: Vec<&String> = value["properties"].as_object().unwrap().keys().collect(); + assert_eq!(remaining, ["b", "d", "e", "g", "h"]); + } +} From 54eeb451e4c083e55d5f9b35692d65304c8abc49 Mon Sep 17 00:00:00 2001 From: AlexRV12 <71396855+AlexRV12@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:31:01 +0200 Subject: [PATCH 20/20] refactor: remove vue as a full-code app framework (#11690) Co-authored-by: Claude Opus 5.5 --- backend/windmill-api/src/apps_raw_bundler.ts | 2 +- cli/src/commands/app/bundle.ts | 22 +- cli/src/commands/app/bundle_command.ts | 4 +- cli/src/commands/app/dev.ts | 6 +- cli/src/commands/app/lint.ts | 2 +- cli/src/commands/app/new.ts | 37 +- cli/src/commands/app/raw_apps.ts | 2 +- cli/src/guidance/skills.gen.ts | 16 +- .../components/copilot/chat/global/core.ts | 9 +- .../components/raw_apps/RawAppEditor.svelte | 5 +- .../src/lib/components/raw_apps/templates.ts | 408 +----------------- .../auto-generated/cli/cli-commands.md | 4 +- system_prompts/auto-generated/prompts.ts | 12 +- .../skills/cli-commands/SKILL.md | 4 +- .../auto-generated/skills/raw-app/SKILL.md | 12 +- system_prompts/base/raw-app-cli.md | 4 +- system_prompts/base/raw-app.md | 8 +- 17 files changed, 52 insertions(+), 505 deletions(-) diff --git a/backend/windmill-api/src/apps_raw_bundler.ts b/backend/windmill-api/src/apps_raw_bundler.ts index 6663a0f83e..965a9289b5 100644 --- a/backend/windmill-api/src/apps_raw_bundler.ts +++ b/backend/windmill-api/src/apps_raw_bundler.ts @@ -6,7 +6,7 @@ * The build itself is `wmill app bundle`, the same one `wmill app push` runs, so * an app deployed through the API is compiled exactly as the CLI and the editor * compile it — entry point, virtual `wmill` module, `/ui/` shared-UI resolution - * and the Svelte/Vue plugins included. Reimplementing any of that here would be + * and the Svelte plugin included. Reimplementing any of that here would be * a third bundler to keep in step with the other two. */ declare const __wmillAppPolicy: { diff --git a/cli/src/commands/app/bundle.ts b/cli/src/commands/app/bundle.ts index 286e878cfb..287d01ebed 100644 --- a/cli/src/commands/app/bundle.ts +++ b/cli/src/commands/app/bundle.ts @@ -58,10 +58,10 @@ function conditionsFor(svelte: boolean): string[] { /** * Detects which frontend frameworks are present in package.json */ -export function detectFrameworks(appDir: string): { svelte: boolean; vue: boolean } { +export function detectFrameworks(appDir: string): { svelte: boolean } { const packageJsonPath = path.join(appDir, "package.json"); if (!fs.existsSync(packageJsonPath)) { - return { svelte: false, vue: false }; + return { svelte: false }; } try { @@ -73,10 +73,9 @@ export function detectFrameworks(appDir: string): { svelte: boolean; vue: boolea return { svelte: "svelte" in allDeps, - vue: "vue" in allDeps, }; } catch { - return { svelte: false, vue: false }; + return { svelte: false }; } } @@ -256,17 +255,6 @@ export async function createFrameworkPlugins(appDir: string): Promise { plugins.push(createSveltePlugin(appDir)); } - if (frameworks.vue) { - log.info(colors.blue("🔧 Vue detected, adding vue plugin...")); - throw new Error("Vue plugin not supported yet"); - // try { - // const esbuildPluginVue = await import("esbuild-plugin-vue3"); - // plugins.push(esbuildPluginVue.default()); - // } catch (error: any) { - // log.warn(colors.yellow(`Failed to load vue plugin: ${error.message}`)); - // } - } - return plugins; } @@ -313,7 +301,7 @@ export async function createBundle( // Use the entryPoint's directory if provided, otherwise fall back to cwd. const appDir = options.entryPoint ? path.dirname(options.entryPoint) : process.cwd(); const frameworks = detectFrameworks(appDir); - const defaultEntry = (frameworks.svelte || frameworks.vue) ? "index.ts" : "index.tsx"; + const defaultEntry = frameworks.svelte ? "index.ts" : "index.tsx"; const entryPoint = options.entryPoint ?? defaultEntry; const outDir = options.outDir ?? "dist"; @@ -331,7 +319,7 @@ export async function createBundle( // Ensure node_modules exists in the app directory await ensureNodeModules(appDir); - // Load framework-specific plugins (svelte, vue) based on package.json + // Load framework-specific plugins (svelte) based on package.json const frameworkPlugins = await createFrameworkPlugins(appDir); // Ensure output directory exists diff --git a/cli/src/commands/app/bundle_command.ts b/cli/src/commands/app/bundle_command.ts index bef13f4268..c4119aafb0 100644 --- a/cli/src/commands/app/bundle_command.ts +++ b/cli/src/commands/app/bundle_command.ts @@ -12,10 +12,10 @@ interface BundleOptions { } /** Every entry point a raw app may have, most specific first, with the - * framework's preferred one promoted — Svelte and Vue mount from a `.ts`. */ + * framework's preferred one promoted — Svelte mounts from a `.ts`. */ function entryPointFor(appDir: string): string { const frameworks = detectFrameworks(appDir); - const candidates = frameworks.svelte || frameworks.vue + const candidates = frameworks.svelte ? ["index.ts", "index.tsx", "index.js"] : ["index.tsx", "index.ts", "index.js"]; const entry = candidates.find((c) => fs.existsSync(path.join(appDir, c))); diff --git a/cli/src/commands/app/dev.ts b/cli/src/commands/app/dev.ts index 2dac8639e2..fdf26fa7da 100644 --- a/cli/src/commands/app/dev.ts +++ b/cli/src/commands/app/dev.ts @@ -481,7 +481,7 @@ async function dev(opts: DevOptions, appFolder?: string) { // Detect frameworks to determine default entry point const frameworks = detectFrameworks(process.cwd()); - const defaultEntry = frameworks.svelte || frameworks.vue + const defaultEntry = frameworks.svelte ? "index.ts" : "index.tsx"; const entryPoint = opts.entry ?? defaultEntry; @@ -577,7 +577,7 @@ async function dev(opts: DevOptions, appFolder?: string) { const buildOptions = getDevBuildOptions(entryPoint, frameworks.svelte); - // Load framework-specific plugins (svelte, vue) based on package.json + // Load framework-specific plugins (svelte) based on package.json const frameworkPlugins = await createFrameworkPlugins(appDir); const wmillPlugin = { @@ -1602,7 +1602,7 @@ const command = new Command() }) .option( "--entry ", - "Entry point file (default: index.ts for Svelte/Vue, index.tsx otherwise)", + "Entry point file (default: index.ts for Svelte, index.tsx otherwise)", ) .option("--no-open", "Don't automatically open the browser") .option( diff --git a/cli/src/commands/app/lint.ts b/cli/src/commands/app/lint.ts index 9356b5dd3c..2276a77cad 100644 --- a/cli/src/commands/app/lint.ts +++ b/cli/src/commands/app/lint.ts @@ -114,7 +114,7 @@ async function validateBuild( // Try to create a bundle - this will validate that all dependencies are in place const frameworks = detectFrameworks(appDir); - const entryFile = frameworks.svelte || frameworks.vue ? "index.ts" : "index.tsx"; + const entryFile = frameworks.svelte ? "index.ts" : "index.tsx"; const entryPoint = path.join(appDir, entryFile); await createBundle({ entryPoint, diff --git a/cli/src/commands/app/new.ts b/cli/src/commands/app/new.ts index b011965ae8..40cc2a4416 100644 --- a/cli/src/commands/app/new.ts +++ b/cli/src/commands/app/new.ts @@ -85,21 +85,6 @@ const app = mount(App, { target: document.getElementById("root")! }); export default app; `; -const appVue = ` - -`; - -const indexVue = `import { createApp } from 'vue' -import App from './App.vue' -import "./index.css"; - -createApp(App).mount('#root')`; - const indexCss = `body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; @@ -171,21 +156,6 @@ const templates: Record = { "svelte": "^5.55.5", "windmill-client": "^1" } -}`, - }, - }, - vue: { - name: "Vue 3", - files: { - "/index.ts": indexVue, - "/App.vue": appVue, - "/index.css": indexCss, - "/package.json": `{ - "dependencies": { - "core-js": "3.26.1", - "vue": "3.5.13", - "windmill-client": "^1" - } }`, }, }, @@ -251,7 +221,7 @@ interface DataConfig { schema?: string; } -type FrameworkKey = "react19" | "react18" | "svelte5" | "vue"; +type FrameworkKey = "react19" | "react18" | "svelte5"; interface NewAppOptions extends GlobalOptions { /** App summary (short description). Skips the prompt when provided. */ @@ -384,7 +354,7 @@ async function newApp(opts: NewAppOptions) { } // Ask for framework (skipped if --framework is provided) - const VALID_FRAMEWORKS: FrameworkKey[] = ["react19", "react18", "svelte5", "vue"]; + const VALID_FRAMEWORKS: FrameworkKey[] = ["react19", "react18", "svelte5"]; let framework: string; if (opts.framework !== undefined) { if (!VALID_FRAMEWORKS.includes(opts.framework)) { @@ -403,7 +373,6 @@ async function newApp(opts: NewAppOptions) { { name: "React 19 (Recommended)", value: "react19" }, { name: "React 18", value: "react18" }, { name: "Svelte 5", value: "svelte5" }, - { name: "Vue 3", value: "vue" }, ], }); } @@ -900,7 +869,7 @@ const command = new Command() ) .option( "--framework ", - "Framework template: react19 | react18 | svelte5 | vue. Skips the prompt when provided. Triggers non-interactive mode." + "Framework template: react19 | react18 | svelte5. Skips the prompt when provided. Triggers non-interactive mode." ) .option( "--datatable ", diff --git a/cli/src/commands/app/raw_apps.ts b/cli/src/commands/app/raw_apps.ts index 36c8260e25..16179d4662 100644 --- a/cli/src/commands/app/raw_apps.ts +++ b/cli/src/commands/app/raw_apps.ts @@ -461,7 +461,7 @@ export async function pushRawApp( log.info(colors.yellow.bold(`Creating raw app ${remotePath} bundle...`)); // Detect frameworks to determine entry point const frameworks = detectFrameworks(localPath); - const entryFile = frameworks.svelte || frameworks.vue + const entryFile = frameworks.svelte ? "index.ts" : "index.tsx"; const entryPoint = localPath + entryFile; diff --git a/cli/src/guidance/skills.gen.ts b/cli/src/guidance/skills.gen.ts index eb5d46ef57..4e8cc464d5 100644 --- a/cli/src/guidance/skills.gen.ts +++ b/cli/src/guidance/skills.gen.ts @@ -6351,7 +6351,7 @@ You need three values to run the command: 1. **summary** — a short description of the app 2. **path** — the windmill path, e.g. \`f/folder/my_app\` or \`u/username/my_app\` -3. **framework** — one of \`react19\` (recommended), \`react18\`, \`svelte5\`, \`vue\` +3. **framework** — one of \`react19\` (recommended), \`react18\`, \`svelte5\` **summary and path**: if the user's request did not supply them, ask. Do not guess values and do not invent paths. @@ -6423,7 +6423,7 @@ my_app{{RAW_APP_SUFFIX}}/ ├── DATATABLES.md # Database schemas (run 'wmill app generate-agents' to refresh) ├── raw_app.yaml # App configuration (summary, path, data settings) ├── index.tsx # Frontend entry point -├── App.tsx # Main React/Svelte/Vue component +├── App.tsx # Main React/Svelte component ├── index.css # Styles ├── package.json # Frontend dependencies ├── wmill.ts # Auto-generated backend type definitions (DO NOT EDIT) @@ -6581,7 +6581,7 @@ For the rest, tell the user which command fits their intent and let them run it # Windmill Raw Apps -Raw apps let you build custom frontends with React, Svelte, or Vue that connect to Windmill backend runnables and datatables. +Raw apps let you build custom frontends with React or Svelte that connect to Windmill backend runnables and datatables. ## App shape @@ -6597,7 +6597,7 @@ A new app gets the setup Windmill's own new-app dialog gives one: a framework, t ### Framework -Use the framework the user named. When they named none, follow what there is to follow: the app being edited, or the apps the request points at. With nothing to follow, use **React 19**, the recommended default, **without asking**. React 18, Svelte 5 and Vue are for when they are asked for. +Use the framework the user named. When they named none, follow what there is to follow: the app being edited, or the apps the request points at. With nothing to follow, use **React 19**, the recommended default, **without asking**. React 18 and Svelte 5 are for when they are asked for. These three are the only frameworks the raw app bundler builds. When the user asks to build or convert an app in another framework, write no code in it: say it is not available and offer these three instead. ### Data setup @@ -6627,7 +6627,7 @@ Then build what was chosen: the data table and schema become the app's \`data.da ### Entrypoint -The entrypoint is \`index.tsx\` for React and \`index.ts\` for Svelte and Vue. It is both the bundling entrypoint (the bundler is esbuild) and the **mount** entrypoint: the preview executes the bundle against an empty \`
\` and auto-renders nothing, so the entrypoint must mount a top-level \`App\` itself. Keep the UI in \`App.tsx\` / \`App.svelte\` / \`App.vue\` and keep the entrypoint as the mount shim. +The entrypoint is \`index.tsx\` for React and \`index.ts\` for Svelte. It is both the bundling entrypoint (the bundler is esbuild) and the **mount** entrypoint: the preview executes the bundle against an empty \`
\` and auto-renders nothing, so the entrypoint must mount a top-level \`App\` itself. Keep the UI in \`App.tsx\` / \`App.svelte\` and keep the entrypoint as the mount shim. React (\`index.tsx\`): @@ -6639,7 +6639,7 @@ import App from './App' createRoot(document.getElementById('root')!).render() \`\`\` -Svelte (\`index.ts\`): \`mount(App, { target: document.getElementById('root')! })\`. Vue (\`index.ts\`): \`createApp(App).mount('#root')\`. +Svelte (\`index.ts\`): \`mount(App, { target: document.getElementById('root')! })\`. **Never replace the entrypoint with a bare component** (\`export default function App() { ... }\` and no mount call). A component that is defined but never mounted renders a blank screen with **no error thrown** — it never executes, so nothing reaches the console or the error overlay. If an app renders blank, check that the entrypoint still mounts \`App\` into \`#root\`. @@ -8102,7 +8102,7 @@ app related commands - \`app dev [app_folder:string]\` - Start a development server for building apps with live reload and hot module replacement - \`--port \` - Port to run the dev server on (will find next available port if occupied) - \`--host \` - Host to bind the dev server to - - \`--entry \` - Entry point file (default: index.ts for Svelte/Vue, index.tsx otherwise) + - \`--entry \` - Entry point file (default: index.ts for Svelte, index.tsx otherwise) - \`--no-open\` - Don't automatically open the browser - \`--recording\` - Frame the app in a shell with a Record button, to capture a replayable session recording of the app under development - \`app lint [app_folder:string]\` - Lint a raw app folder to validate structure and buildability @@ -8113,7 +8113,7 @@ app related commands - \`app new\` - create a new raw app from a template - \`--summary \` - App summary (short description). Skips the prompt when provided. Triggers non-interactive mode. - \`--path \` - App path (e.g., f/folder/my_app or u/username/my_app). Skips the prompt when provided. Triggers non-interactive mode. - - \`--framework \` - Framework template: react19 | react18 | svelte5 | vue. Skips the prompt when provided. Triggers non-interactive mode. + - \`--framework \` - Framework template: react19 | react18 | svelte5. Skips the prompt when provided. Triggers non-interactive mode. - \`--datatable \` - Datatable to wire up. Without this flag in non-interactive mode, no datatable is configured. - \`--schema \` - Schema to use with --datatable. If it doesn't exist yet, a sql_to_apply/ migration (CREATE SCHEMA IF NOT EXISTS) is written for it. - \`--overwrite\` - Overwrite the target directory if it already exists, without prompting. diff --git a/frontend/src/lib/components/copilot/chat/global/core.ts b/frontend/src/lib/components/copilot/chat/global/core.ts index 2a795f1f81..5295712626 100644 --- a/frontend/src/lib/components/copilot/chat/global/core.ts +++ b/frontend/src/lib/components/copilot/chat/global/core.ts @@ -1306,12 +1306,7 @@ const readDomSchema = z.object({ const takeScreenshotSchema = z.object({}) -const FRAMEWORK_KEYS = [ - 'react19', - 'react18', - 'svelte5', - 'vue' -] as const satisfies readonly FrameworkKey[] +const FRAMEWORK_KEYS = ['react19', 'react18', 'svelte5'] as const satisfies readonly FrameworkKey[] const initAppSchema = z.object({ path: z @@ -2493,7 +2488,7 @@ function getAppInstructions(language?: ScriptLang): string { - Global mode edits raw app drafts only; it does not save or deploy unless the user explicitly asks to deploy. - App drafts are addressed by workspace path. Follow the path conventions in the system prompt: default to \`u//\` for bare names; only use \`f//\` when the folder is known to exist. The first write tool snapshots the workspace app onto the draft, and subsequent writes accumulate. -- To create a new app, use \`init_app\` with a path, optional summary, a framework (\`react19\` / \`react18\` / \`svelte5\` / \`vue\`) and, for an app that stores data, its \`data\` setup. "Starting a new app" below decides the framework and when the data setup is asked. Ask each of its questions with \`askUserQuestion\`, one question per call with the suggested answer first, before \`init_app\` and before any SQL. \`init_app\` only records \`data\`: a new schema is yours to create with the datatable SQL tool. \`init_app\` errors if an app already exists at the path or a draft is already in flight; in that case, edit the existing one rather than re-initializing. +- To create a new app, use \`init_app\` with a path, optional summary, a framework (\`react19\` / \`react18\` / \`svelte5\`) and, for an app that stores data, its \`data\` setup. "Starting a new app" below decides the framework and when the data setup is asked. Ask each of its questions with \`askUserQuestion\`, one question per call with the suggested answer first, before \`init_app\` and before any SQL. \`init_app\` only records \`data\`: a new schema is yours to create with the datatable SQL tool. \`init_app\` errors if an app already exists at the path or a draft is already in flight; in that case, edit the existing one rather than re-initializing. - \`init_app\` seeds a starter inline runnable named \`a\` (bun, \`main(x: string) => string\`) so the React/Svelte demo button works on first render. Replace or remove it once you start building real backend runnables. - Frontend file paths start with \`/\` (e.g. \`/index.tsx\`, \`/App.tsx\`, \`/styles.css\`). Use \`write_app_file\` / \`patch_app_file\` / \`delete_app_file\`. - Backend inline runnables are addressed as \`backend//main.{ts|py}\` from the file tools, but you create or update them via \`write_app_runnable\` / \`delete_app_runnable\` (which take the runnable shape directly: \`{ name, type, inlineScript?, path?, staticInputs? }\`). diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte index f708b97efe..46c23b79b3 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte @@ -496,7 +496,7 @@ addEventListener('message', function (e) { // entrypoint defines a component without ever mounting it. Cleared on next build. let emptyRender = $state(false) // The repair hint above has to match the app's framework: only React apps have - // an `index.tsx` and `createRoot`; Svelte and Vue mount from `index.ts`. Keyed + // an `index.tsx` and `createRoot`; Svelte mounts from `index.ts`. Keyed // off file extensions rather than exact template filenames, which users rename. let mountHint = $derived.by(() => { const paths = Object.keys(files ?? {}).map((p) => p.replace(/^\//, '')) @@ -507,9 +507,6 @@ addEventListener('message', function (e) { call: "mount(App, { target: document.getElementById('root')! })" } } - if (paths.some((p) => p.endsWith('.vue'))) { - return { entrypoint: entrypoint ?? 'index.ts', call: "createApp(App).mount('#root')" } - } return { entrypoint: entrypoint ?? 'index.tsx', call: "createRoot(document.getElementById('root')!).render()" diff --git a/frontend/src/lib/components/raw_apps/templates.ts b/frontend/src/lib/components/raw_apps/templates.ts index 50eae99e97..00d7594386 100644 --- a/frontend/src/lib/components/raw_apps/templates.ts +++ b/frontend/src/lib/components/raw_apps/templates.ts @@ -1,5 +1,5 @@ // The starter app is a tour of what a raw app can do: each card calls one of the -// STARTER_RUNNABLES below through a different `./wmill` export. The three framework +// STARTER_RUNNABLES below through a different `./wmill` export. The framework // variants share `indexCss` and must stay in step with each other and with the runnables. const reactIndex = ` @@ -13,7 +13,7 @@ root.render(); ` const STATS_SAMPLE_TEXT = - 'Windmill turns scripts into apps. Write the frontend in React, Svelte or Vue, and the backend in TypeScript, Python or any other supported language. Scripts run on workers, and the app calls them like functions.' + 'Windmill turns scripts into apps. Write the frontend in React or Svelte, and the backend in TypeScript, Python or any other supported language. Scripts run on workers, and the app calls them like functions.' const appTsx = `import React, { useState } from 'react' import { backend, backendAsync, getJob, streamJob } from './wmill' @@ -516,230 +516,6 @@ const app = mount(App, { target: document.getElementById("root")! }); export default app; ` -const appVue = ` - -` - -const indexVue = `import { createApp } from 'vue' -import App from './App.vue' -import "./index.css"; - -createApp(App).mount('#root')` - const indexCss = `/* The app renders inside an iframe whose host page has its own theme the iframe cannot read, so paint an opaque surface here: without one the host's background shows through and dark text lands on it unreadable. */ @@ -1158,23 +934,10 @@ export const svelte5Template = { }` } -export const vueTemplate = { - '/index.ts': indexVue, - '/App.vue': appVue, - '/index.css': indexCss, - '/package.json': `{ - "dependencies": { - "core-js": "3.26.1", - "vue": "3.5.13" - } -}` -} - export const FRAMEWORK_TEMPLATES = { react19: react19Template, react18: react18Template, - svelte5: svelte5Template, - vue: vueTemplate + svelte5: svelte5Template } export type FrameworkKey = keyof typeof FRAMEWORK_TEMPLATES @@ -1551,171 +1314,6 @@ export async function main() { } } -export const appVueRouter = ` - - - - -` - export const appSvelteRouter = `