diff --git a/.claude/settings.json b/.claude/settings.json index 0596b17e91..1ef3704831 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -55,7 +55,10 @@ "Read(**/*.pem)", "Read(**/*.key)", "Read(**/credentials.json)", - "Read(**/*secret*)", + "Read(**/.secret*)", + "Read(**/.secrets*)", + "Read(**/*.secret)", + "Read(**/*.secrets)", "Edit(.env)", "Edit(.env.*)", "Edit(**/.env)", diff --git a/.github/workflows/codex-pr-review.yml b/.github/workflows/codex-pr-review.yml index e89a58b629..b4916714f2 100644 --- a/.github/workflows/codex-pr-review.yml +++ b/.github/workflows/codex-pr-review.yml @@ -82,10 +82,11 @@ jobs: EVENT_TITLE: ${{ github.event.pull_request.title }} EVENT_BODY: ${{ github.event.pull_request.body }} EVENT_FORK: ${{ github.event.pull_request.head.repo.fork }} + EVENT_AUTHOR: ${{ github.event.pull_request.user.login }} run: | if [ -n "$INPUT_PR_NUMBER" ]; then PR_JSON=$(gh pr view "$INPUT_PR_NUMBER" --repo "${{ github.repository }}" \ - --json number,baseRefName,baseRefOid,headRefOid,title,body,isCrossRepository) + --json number,baseRefName,baseRefOid,headRefOid,title,body,isCrossRepository,author) PR_NUMBER=$(echo "$PR_JSON" | jq -r '.number') BASE_REF=$(echo "$PR_JSON" | jq -r '.baseRefName') BASE_SHA=$(echo "$PR_JSON" | jq -r '.baseRefOid') @@ -93,6 +94,7 @@ jobs: PR_TITLE=$(echo "$PR_JSON" | jq -r '.title') PR_BODY=$(echo "$PR_JSON" | jq -r '.body // ""') IS_FORK=$(echo "$PR_JSON" | jq -r '.isCrossRepository') + PR_AUTHOR=$(echo "$PR_JSON" | jq -r '.author.login // ""') else PR_NUMBER="$EVENT_PR_NUMBER" BASE_REF="$EVENT_BASE_REF" @@ -101,6 +103,7 @@ jobs: PR_TITLE="$EVENT_TITLE" PR_BODY="$EVENT_BODY" IS_FORK="$EVENT_FORK" + PR_AUTHOR="$EVENT_AUTHOR" fi if [ "$IS_FORK" = "true" ]; then echo "Skipping Codex review for fork PR." @@ -113,6 +116,7 @@ jobs: echo "base_ref=$BASE_REF" echo "base_sha=$BASE_SHA" echo "head_sha=$HEAD_SHA" + echo "pr_author=$PR_AUTHOR" echo 'title<> "$GITHUB_OUTPUT" + PR_NUMBER="$INPUT_PR_NUMBER" + PR_AUTHOR=$(gh api "repos/$REPO/pulls/$PR_NUMBER" --jq '.user.login') else - echo "pr_number=$EVENT_PR_NUMBER" >> "$GITHUB_OUTPUT" + PR_NUMBER="$EVENT_PR_NUMBER" + PR_AUTHOR="$EVENT_PR_AUTHOR" fi + echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" + echo "pr_author=$PR_AUTHOR" >> "$GITHUB_OUTPUT" - name: Fetch prior PR discussion id: prior @@ -148,6 +155,7 @@ jobs: prompt: | REPO: ${{ github.repository }} PR NUMBER: ${{ steps.resolve.outputs.pr_number }} + PR AUTHOR: ${{ steps.resolve.outputs.pr_author }} ${{ env.REVIEW_PROMPT }} claude_args: | diff --git a/CHANGELOG.md b/CHANGELOG.md index a7f2f43e3a..e297309df2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,84 @@ # Changelog +## [1.700.2](https://github.com/windmill-labs/windmill/compare/v1.700.1...v1.700.2) (2026-05-12) + + +### Bug Fixes + +* preserve explicit nulls for typed fields in bulk instance config ([#9123](https://github.com/windmill-labs/windmill/issues/9123)) ([cab0000](https://github.com/windmill-labs/windmill/commit/cab0000f3a5e9a0b201a85da1a01b1f82df8a316)) +* preserve negative integers in Bedrock tool schema conversion ([#9116](https://github.com/windmill-labs/windmill/issues/9116)) ([01e21c7](https://github.com/windmill-labs/windmill/commit/01e21c7f913eaf7dffc3d6a31501418ff2104c8b)) + +## [1.700.1](https://github.com/windmill-labs/windmill/compare/v1.700.0...v1.700.1) (2026-05-11) + + +### Bug Fixes + +* CE build broken by enterprise-gated compute_instance_hash ([#9113](https://github.com/windmill-labs/windmill/issues/9113)) ([cd65de4](https://github.com/windmill-labs/windmill/commit/cd65de49285ff60abdd94c883180ded65609f382)) + +## [1.700.0](https://github.com/windmill-labs/windmill/compare/v1.699.0...v1.700.0) (2026-05-11) + + +### Features + +* **cli:** auto-infer args for `wmill app push` ([#9091](https://github.com/windmill-labs/windmill/issues/9091)) ([43b1800](https://github.com/windmill-labs/windmill/commit/43b18006f32fd5db54bbf8ae7ff0e0b314a517e5)) +* **forks:** prompt to delete forked children when deleting a fork ([#9097](https://github.com/windmill-labs/windmill/issues/9097)) ([e43a958](https://github.com/windmill-labs/windmill/commit/e43a958c5c6ae01a1fbecf3db63c6541a245be62)) +* **operators:** allow operators to access assets page ([#9095](https://github.com/windmill-labs/windmill/issues/9095)) ([20ecd90](https://github.com/windmill-labs/windmill/commit/20ecd904e7060c3cf90f2605740bb349b2a3e6ed)) +* **vault:** configurable JWT auth mount path and setup-doc fixes ([#9100](https://github.com/windmill-labs/windmill/issues/9100)) ([f8ba084](https://github.com/windmill-labs/windmill/commit/f8ba0840d74572c880cf458938365b3ec808c6fb)) + + +### Bug Fixes + +* add Input, Result, Trigger to reserved flow step IDs ([#9109](https://github.com/windmill-labs/windmill/issues/9109)) ([9f79a86](https://github.com/windmill-labs/windmill/commit/9f79a86a686708f66ccc512d4f132cb9a00397a7)), closes [#7139](https://github.com/windmill-labs/windmill/issues/7139) +* **frontend:** mark Path dirty when folder picker changes selection ([#9096](https://github.com/windmill-labs/windmill/issues/9096)) ([23bb1b5](https://github.com/windmill-labs/windmill/commit/23bb1b541e78846d5978153fd8d9bb4f01cec72b)) +* mask oauth client secret in instance settings ([#9112](https://github.com/windmill-labs/windmill/issues/9112)) ([ac3c155](https://github.com/windmill-labs/windmill/commit/ac3c155541eb5ca20d65c38ad13dca6c10a572c9)) +* populate raw_code for flowscript and appscript runs ([#9104](https://github.com/windmill-labs/windmill/issues/9104)) ([05172ac](https://github.com/windmill-labs/windmill/commit/05172ac3bdfc3472da5e9d8a825cdd479ba9e375)) + + +### Performance Improvements + +* lazy-load script editor history and hit partial index ([#9107](https://github.com/windmill-labs/windmill/issues/9107)) ([03e8bc8](https://github.com/windmill-labs/windmill/commit/03e8bc8c14258355d7d695333c1588807fbf8cd6)) + +## [1.699.0](https://github.com/windmill-labs/windmill/compare/v1.698.0...v1.699.0) (2026-05-08) + + +### Features + +* parse windmill_failure field to tag run as failure ([#9073](https://github.com/windmill-labs/windmill/issues/9073)) ([dd53202](https://github.com/windmill-labs/windmill/commit/dd5320205f200dd058db2ff7d44d5c4bbcf25ec9)) + + +### Bug Fixes + +* **cli:** bump svelte version in `wmill app new` template ([#9084](https://github.com/windmill-labs/windmill/issues/9084)) ([4b4aa0e](https://github.com/windmill-labs/windmill/commit/4b4aa0e303f9c47c4f931511977107f42f93abc3)) +* **flows:** populate error handler input args from failure picker ([#9087](https://github.com/windmill-labs/windmill/issues/9087)) ([f37d360](https://github.com/windmill-labs/windmill/commit/f37d3606446d23f8b11a94ea1ce5f5d4836fae17)) +* hide _ENTRYPOINT_OVERRIDE jobs from script/flow history panel ([#9088](https://github.com/windmill-labs/windmill/issues/9088)) ([935c666](https://github.com/windmill-labs/windmill/commit/935c666d50ef30d89a3669c76094af7506fbb448)) +* **native-triggers:** serialize Google channel renewal across replicas ([#9060](https://github.com/windmill-labs/windmill/issues/9060)) ([ee3d82f](https://github.com/windmill-labs/windmill/commit/ee3d82f01f52d835218f544dad6de9b7c3184fbb)) +* **python:** verify wheel RECORD on cache pull/install, finalize piptar ([#9090](https://github.com/windmill-labs/windmill/issues/9090)) ([98ff146](https://github.com/windmill-labs/windmill/commit/98ff146cfabf45418c95c027ad6d07b08069cfcd)) +* reject root-rooted paths in ansible playbook validator on windows ([#9081](https://github.com/windmill-labs/windmill/issues/9081)) ([d37277d](https://github.com/windmill-labs/windmill/commit/d37277d2341c83faf72efa0035cbf70e2cfbd596)) + + +### Performance Improvements + +* **flows:** gate flow_env resolve on expr text and share cache with handle_flow ([#9085](https://github.com/windmill-labs/windmill/issues/9085)) ([23af6c2](https://github.com/windmill-labs/windmill/commit/23af6c2ea31265a1898d0632e72cd2fd826e4044)) + +## [1.698.0](https://github.com/windmill-labs/windmill/compare/v1.697.0...v1.698.0) (2026-05-08) + + +### Features + +* **cli:** add --parallel flag to generate-metadata ([#9074](https://github.com/windmill-labs/windmill/issues/9074)) ([bc527fd](https://github.com/windmill-labs/windmill/commit/bc527fd929577ac57d4e24196069ed236b702d71)) + + +### Bug Fixes + +* **cli-tests:** stabilize flow lock-gen race + Windows path ([#9080](https://github.com/windmill-labs/windmill/issues/9080)) ([1c56148](https://github.com/windmill-labs/windmill/commit/1c56148714861aafc4f489916c71aa4674e938c0)) +* **cli:** forward HEADERS env var on every backend fetch call ([#9075](https://github.com/windmill-labs/windmill/issues/9075)) ([d647686](https://github.com/windmill-labs/windmill/commit/d6476862b30692e450cceda09c58d47964f87d32)) + + +### Performance Improvements + +* **flows:** cache resolved flow_env per flow execution ([#9079](https://github.com/windmill-labs/windmill/issues/9079)) ([e1a7c75](https://github.com/windmill-labs/windmill/commit/e1a7c75e192b72b3b0d854c1901653e0b9386bf2)) +* **flows:** skip flow_env DB+transform work when no resolution is needed ([#9078](https://github.com/windmill-labs/windmill/issues/9078)) ([2067e07](https://github.com/windmill-labs/windmill/commit/2067e0719fd1fd1b899b015badec0f222c054e66)) + ## [1.697.0](https://github.com/windmill-labs/windmill/compare/v1.696.2...v1.697.0) (2026-05-07) diff --git a/REVIEW.md b/REVIEW.md index de311cccc6..21f5e50f26 100644 --- a/REVIEW.md +++ b/REVIEW.md @@ -9,7 +9,7 @@ You are reviewing a GitHub pull request for this repository. Apply this policy a ## Verdict (first line of the review) -Start every review with a single verdict line, before any other section. Pick exactly one: +Start every review with a single verdict line, before any other section (the only thing that may appear above the verdict is the optional `cc @` ping described in "Pinging the author" below). Pick exactly one: - **Good to merge** — no blocking issues and no nits worth surfacing. - **Mergeable, but should ideally address nits: ** — no blockers, but P2 findings that are worth a look. The list must name each nit briefly (e.g. "doc/code mismatch in `foo.rs`, half-finished `pub fn bar`"). @@ -17,6 +17,10 @@ Start every review with a single verdict line, before any other section. Pick ex The names in the list must match findings detailed later in the review. If you list a nit or issue here, it must appear with full context in the body. Do not invent items that aren't in the body, and do not bury blockers in the body without surfacing them in the verdict. +## Pinging the author + +If the prompt context provides a `PR AUTHOR` (GitHub login) and the verdict is NOT "Good to merge" (i.e. it is "Mergeable, but should ideally address nits: ..." or "Should address issues before merging: ..."), prepend a single line `cc @` to the top-level review comment, above the verdict line. This pings the author so they get a notification that there are items to address. Skip the ping entirely when the verdict is "Good to merge" — there is nothing for the author to act on. Do not add the ping to inline comments; the top-level summary comment is the only place it belongs. + ## Review policy - Only report issues you are confident are real and introduced by this pull request. diff --git a/ai_evals/README.md b/ai_evals/README.md index 267451aabf..2e1f3210f8 100644 --- a/ai_evals/README.md +++ b/ai_evals/README.md @@ -55,7 +55,7 @@ bun run cli -- run flow flow-test4-order-processing-loop --model opus bun run cli -- run flow flow-test0-sum-two-numbers --models haiku,opus,4o bun run cli -- run flow flow-test0-sum-two-numbers --runs 3 --verbose bun run cli -- run flow --record -GEMINI_API_KEY=... bun run cli -- run app app-test1-counter-create --model gemini-pro --transport proxy +GEMINI_API_KEY=... bun run cli -- run app app-test1-counter-create --model gemini-pro WMILL_AI_EVAL_BACKEND_URL=http://127.0.0.1:8000 bun run cli -- run flow --backend-validation preview bun run cli -- run cli bun-hello-script ``` @@ -72,7 +72,6 @@ Public CLI surface: - `--output `: custom result JSON path - `--model `: choose the model under test - `--models `: run the same cases sequentially against several model aliases -- `--transport `: frontend request transport (`direct` by default, `proxy` to exercise `/api/w/{workspace}/ai/proxy`) - `--verbose`: stream assistant output for frontend runs - `--record`: append a compact tracked summary line to `ai_evals/history/.jsonl` for full-suite runs only - `--backend-validation `: optional backend smoke validation (`off` or `preview`) for `script` and `flow` evals @@ -145,26 +144,23 @@ If `--backend-validation preview` is enabled: - `script` evals run a real backend script preview in an isolated temp workspace - `flow` evals run a real backend flow preview only for cases that define `runtime.backendPreview` - `flow` cases with `initial.workspace` fixtures seed those scripts and flows into the preview workspace before preview -- when `WMILL_AI_EVAL_BACKEND_WORKSPACE` is set, `ai_evals` treats that workspace as a dedicated test workspace, clears managed eval assets under `f/evals/*` before each preview run, and then reseeds the current case fixtures +- when `WMILL_AI_EVAL_BACKEND_WORKSPACE` is set, `ai_evals` creates or reuses that workspace as a dedicated test workspace, clears managed eval assets under `f/evals/*` before each preview run, and then reseeds the current case fixtures -Supported backend validation env vars: +Supported backend env vars: - `WMILL_AI_EVAL_BACKEND_VALIDATION=preview` - `WMILL_AI_EVAL_BACKEND_URL=http://127.0.0.1:8000` - `WMILL_AI_EVAL_BACKEND_EMAIL=admin@windmill.dev` - `WMILL_AI_EVAL_BACKEND_PASSWORD=changeme` - `WMILL_AI_EVAL_BACKEND_WORKSPACE=integration-tests` to reuse an existing workspace on CE installs with low workspace limits -- `WMILL_AI_EVAL_KEEP_WORKSPACES=1` -- `WMILL_AI_EVAL_WORKSPACE_PREFIX=ai-evals` -Frontend proxy transport uses the same backend auth/workspace env vars. +Frontend modes require a reachable Windmill backend and send model requests through the workspace AI proxy at `/api/w/{workspace}/ai/proxy`. At startup, `ai_evals` checks the resolved backend URL and fails early with setup guidance if the backend cannot be reached or login fails. -When `--transport proxy` is set: +For frontend modes: -- `ai_evals` creates or reuses a backend workspace +- `ai_evals` creates a temporary backend workspace, or creates/reuses `WMILL_AI_EVAL_BACKEND_WORKSPACE` when it is set - it upserts a provider resource under `f/evals/ai/` - frontend requests go through `/api/w/{workspace}/ai/proxy` -- result JSON and history records include `transport` so direct vs proxy runs stay distinguishable ## Results And Artifacts @@ -182,7 +178,7 @@ If `--record` is used, the CLI also appends one compact JSON line to: Each recorded line contains: -- run metadata (`createdAt`, `gitSha`, `mode`, `runModel`, `transport`, `judgeModel`) +- run metadata (`createdAt`, `gitSha`, `mode`, `runModel`, `judgeModel`) - suite totals (`caseCount`, `attemptCount`, `passedAttempts`, `passRate`, `averageDurationMs`, `averageJudgeScore`) - average token usage (`averageTokenUsagePerAttempt`) - per-case metrics under `cases[]` (`averageDurationMs`, `averageJudgeScore`, `averageTokenUsagePerAttempt`, pass rate) diff --git a/ai_evals/adapters/frontend/backendPreview.test.ts b/ai_evals/adapters/frontend/backendPreview.test.ts index 2f12c9a896..d4de361333 100644 --- a/ai_evals/adapters/frontend/backendPreview.test.ts +++ b/ai_evals/adapters/frontend/backendPreview.test.ts @@ -210,8 +210,6 @@ function buildSettings( baseUrl: 'http://backend.test/default', email: 'admin@windmill.dev', password: 'changeme', - keepWorkspaces: true, - workspacePrefix: 'ai-evals', pollIntervalMs: 1, maxWaitMs: 50, ...overrides diff --git a/ai_evals/adapters/frontend/backendPreview.ts b/ai_evals/adapters/frontend/backendPreview.ts index e1be934564..57e1cfdf2a 100644 --- a/ai_evals/adapters/frontend/backendPreview.ts +++ b/ai_evals/adapters/frontend/backendPreview.ts @@ -24,6 +24,7 @@ export interface CompletedPreviewJob { const tokenCache = new Map>() const sharedWorkspaceQueue = new Map>() const managedSharedWorkspacePrefixes = ['f/evals/'] +const DEFAULT_WORKSPACE_PREFIX = 'ai-evals' export class BackendPreviewClient { constructor(private readonly settings: BackendValidationSettings) {} @@ -35,7 +36,7 @@ export class BackendPreviewClient { ): Promise { const workspaceId = this.settings.workspaceOverride ?? - buildWorkspaceId(this.settings.workspacePrefix, caseId, attempt) + buildWorkspaceId(caseId, attempt) const run = async () => { await this.ensureWorkspace(workspaceId) @@ -46,7 +47,7 @@ export class BackendPreviewClient { try { return await body(workspaceId) } finally { - if (!this.settings.keepWorkspaces && !this.settings.workspaceOverride) { + if (!this.settings.workspaceOverride) { await this.deleteWorkspace(workspaceId).catch(() => undefined) } } @@ -440,14 +441,14 @@ async function withSharedWorkspaceLock(workspaceId: string, body: () => Promi } } -function buildWorkspaceId(prefix: string, caseId: string, attempt: number): string { +function buildWorkspaceId(caseId: string, attempt: number): string { const caseSlug = caseId .toLowerCase() .replace(/[^a-z0-9-]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 30) const suffix = randomUUID().slice(0, 8) - return `${prefix}-${caseSlug || 'case'}-a${attempt}-${suffix}` + return `${DEFAULT_WORKSPACE_PREFIX}-${caseSlug || 'case'}-a${attempt}-${suffix}` } function extractFolderName(path: string): string | null { diff --git a/ai_evals/adapters/frontend/benchmarkRunner.ts b/ai_evals/adapters/frontend/benchmarkRunner.ts index 474d434803..50a0d10c3c 100644 --- a/ai_evals/adapters/frontend/benchmarkRunner.ts +++ b/ai_evals/adapters/frontend/benchmarkRunner.ts @@ -1,6 +1,5 @@ import { loadSelectedCases } from "../../core/cases"; import { resolveBackendValidationSettings } from "../../core/backendValidation"; -import { resolveFrontendEvalTransportSettings } from "../../core/frontendTransport"; import { formatRunModelLabel, getFrontendEvalModel, @@ -9,6 +8,7 @@ import { import { buildRunResult } from "../../core/results"; import { runSuite } from "../../core/runSuite"; import type { BenchmarkRunResult, ModeRunner } from "../../core/types"; +import { resolveWindmillBackendSettings } from "../../core/windmillBackendSettings"; import { emitFrontendBenchmarkProgress } from "./progress"; import { createAppModeRunner } from "../../modes/app"; import { createFlowModeRunner } from "../../modes/flow"; @@ -36,17 +36,14 @@ export async function runFrontendBenchmarkFromEnv(): Promise evalMode: mode, requestedMode: process.env.WMILL_FRONTEND_AI_EVAL_BACKEND_VALIDATION, }); - const transportSettings = resolveFrontendEvalTransportSettings({ - evalMode: mode, - requestedTransport: process.env.WMILL_FRONTEND_AI_EVAL_TRANSPORT, - }); + const backendSettings = resolveWindmillBackendSettings(); const selectedCases = await loadSelectedCases(mode, caseIds); const modeRunner = getModeRunner( mode, getFrontendEvalModel(model), backendValidation, - transportSettings, + backendSettings, ); const runModel = formatRunModelLabel(mode, model); const caseResults = await runSuite({ @@ -66,7 +63,6 @@ export async function runFrontendBenchmarkFromEnv(): Promise mode, runs, runModel, - transport: transportSettings.transport, judgeModel: DEFAULT_JUDGE_MODEL, caseResults, }); @@ -76,18 +72,18 @@ function getModeRunner( mode: FrontendBenchmarkMode, model: ReturnType, backendValidation: ReturnType, - transportSettings: ReturnType, + backendSettings: ReturnType, ): ModeRunner { switch (mode) { case "flow": - return createFlowModeRunner(model, backendValidation, transportSettings); + return createFlowModeRunner(model, backendValidation, backendSettings); case "app": - return createAppModeRunner(model, transportSettings); + return createAppModeRunner(model, backendSettings); case "script": return createScriptModeRunner( model, backendValidation, - transportSettings, + backendSettings, ); } } diff --git a/ai_evals/adapters/frontend/core/app/appEvalRunner.ts b/ai_evals/adapters/frontend/core/app/appEvalRunner.ts index 55d1e6ab9a..16543b28de 100644 --- a/ai_evals/adapters/frontend/core/app/appEvalRunner.ts +++ b/ai_evals/adapters/frontend/core/app/appEvalRunner.ts @@ -12,7 +12,7 @@ import { prepareAppUserMessage, } from "../../../../../frontend/src/lib/components/copilot/chat/app/core"; import type { Tool as ProductionTool } from "../../../../../frontend/src/lib/components/copilot/chat/shared"; -import { createAppFileHelpers } from "./fileHelpers"; +import { createAppFileHelpers, type AppEvalChatHelpers } from "./fileHelpers"; import { runEval } from "../shared"; import type { AIProvider } from "$lib/gen/types.gen"; import type { @@ -22,7 +22,6 @@ import type { } from "../../../../core/types"; import type { TokenUsage } from "../shared/types"; import type { AppFilesState } from "../../../../core/validators"; -import type { FrontendEvalTransport } from "../../../../core/frontendTransport"; import type { WindmillBackendSettings } from "../../../../core/windmillBackendSettings"; import { createAppBackendRunnableContextElement, @@ -49,8 +48,7 @@ export interface AppEvalOptions { model?: string; maxIterations?: number; provider?: AIProvider; - transport?: FrontendEvalTransport; - backend?: WindmillBackendSettings; + backend: WindmillBackendSettings; workspaceRoot?: string; runContext?: ModeRunContext; } @@ -58,7 +56,7 @@ export interface AppEvalOptions { export async function runAppEval( userPrompt: string, apiKey: string, - options?: AppEvalOptions, + options: AppEvalOptions, ): Promise { const workspaceRoot = options?.workspaceRoot ?? @@ -101,10 +99,9 @@ export async function runAppEval( model, workspace: workspaceRoot, provider: options?.provider, - transport: options?.transport, - backend: options?.backend, - proxyCaseId: options?.runContext?.caseId, - proxyAttempt: options?.runContext?.attempt, + backend: options.backend, + caseId: options?.runContext?.caseId, + attempt: options?.runContext?.attempt, }, }); @@ -124,7 +121,7 @@ export async function runAppEval( async function buildAdditionalContext( appContext: EvalCaseRuntimeAppContextSpec | undefined, - helpers: AppAIChatHelpers, + helpers: AppEvalChatHelpers, ): Promise { const entries = appContext?.additional ?? []; if (entries.length === 0) { diff --git a/ai_evals/adapters/frontend/core/app/fileHelpers.ts b/ai_evals/adapters/frontend/core/app/fileHelpers.ts index e82ddf4672..15721a49ed 100644 --- a/ai_evals/adapters/frontend/core/app/fileHelpers.ts +++ b/ai_evals/adapters/frontend/core/app/fileHelpers.ts @@ -2,6 +2,7 @@ import { mkdir, rm, writeFile } from 'fs/promises' import { dirname, join } from 'path' import type { AppAIChatHelpers, + AppDatatableMetadata, AppFiles, BackendRunnable, DataTableSchema, @@ -10,6 +11,10 @@ import type { } from '../../../../../frontend/src/lib/components/copilot/chat/app/core' import { buildAppWmillTypes, collectAppDiagnostics } from '../../../../core/appDiagnostics' +export interface AppEvalChatHelpers extends AppAIChatHelpers { + getDatatables: () => Promise +} + async function writeFrontendFile( workspaceRoot: string | undefined, path: string, @@ -92,7 +97,7 @@ export async function createAppFileHelpers( initialDatatables: DataTableSchema[] = [], workspaceRoot?: string ): Promise<{ - helpers: AppAIChatHelpers + helpers: AppEvalChatHelpers getFiles: () => AppFiles getEvalState: () => { frontend: Record @@ -137,7 +142,7 @@ export async function createAppFileHelpers( } await persistDatatables(workspaceRoot, datatables) - const helpers: AppAIChatHelpers = { + const helpers: AppEvalChatHelpers = { listFrontendFiles: () => [ ...Object.keys(frontend).filter((path) => path !== '/wmill.d.ts'), '/wmill.d.ts' @@ -211,6 +216,34 @@ export async function createAppFileHelpers( }, lint, getDatatables: async () => structuredClone(datatables), + listDatatableTables: async () => + datatables.map( + (datatable): AppDatatableMetadata => { + const schemas = Object.fromEntries( + Object.entries(datatable.schemas).map(([schemaName, tables]) => [ + schemaName, + Object.keys(tables) + ]) + ) + return { + datatable_name: datatable.datatable_name, + schemas, + tableCount: Object.values(schemas).reduce( + (sum, tableNames) => sum + tableNames.length, + 0 + ), + error: datatable.error + } + } + ), + getDatatableTableSchema: async ( + datatableName: string, + schemaName: string, + tableName: string + ) => { + const datatable = datatables.find((entry) => entry.datatable_name === datatableName) + return structuredClone(datatable?.schemas?.[schemaName]?.[tableName] ?? {}) + }, getAvailableDatatableNames: () => datatables.map((datatable) => datatable.datatable_name), execDatatableSql: async ( datatableName: string, diff --git a/ai_evals/adapters/frontend/core/flow/flowEvalRunner.ts b/ai_evals/adapters/frontend/core/flow/flowEvalRunner.ts index 1b448bdea4..0cd25f5787 100644 --- a/ai_evals/adapters/frontend/core/flow/flowEvalRunner.ts +++ b/ai_evals/adapters/frontend/core/flow/flowEvalRunner.ts @@ -18,7 +18,6 @@ import { import { runEval } from "../shared"; import type { ModeRunContext } from "../../../../core/types"; import type { TokenUsage, ToolCallDetail } from "../shared/types"; -import type { FrontendEvalTransport } from "../../../../core/frontendTransport"; import type { WindmillBackendSettings } from "../../../../core/windmillBackendSettings"; export interface FlowFixture { @@ -48,8 +47,7 @@ export interface FlowEvalOptions { model?: string; maxIterations?: number; provider?: AIProvider; - transport?: FrontendEvalTransport; - backend?: WindmillBackendSettings; + backend: WindmillBackendSettings; workspaceRoot?: string; runContext?: ModeRunContext; } @@ -57,7 +55,7 @@ export interface FlowEvalOptions { export async function runFlowEval( userPrompt: string, apiKey: string, - options?: FlowEvalOptions, + options: FlowEvalOptions, ): Promise { const workspaceRoot = options?.workspaceRoot ?? @@ -100,10 +98,9 @@ export async function runFlowEval( model, workspace: workspaceRoot, provider: options?.provider, - transport: options?.transport, - backend: options?.backend, - proxyCaseId: options?.runContext?.caseId, - proxyAttempt: options?.runContext?.attempt, + backend: options.backend, + caseId: options?.runContext?.caseId, + attempt: options?.runContext?.attempt, }, }); diff --git a/ai_evals/adapters/frontend/core/script/scriptEvalRunner.ts b/ai_evals/adapters/frontend/core/script/scriptEvalRunner.ts index 30aa2c81e7..95ce6555e6 100644 --- a/ai_evals/adapters/frontend/core/script/scriptEvalRunner.ts +++ b/ai_evals/adapters/frontend/core/script/scriptEvalRunner.ts @@ -14,7 +14,6 @@ import { createScriptFileHelpers, type ScriptEvalState } from "./fileHelpers"; import { runEval } from "../shared"; import type { ModeRunContext } from "../../../../core/types"; import type { TokenUsage, ToolCallDetail } from "../shared/types"; -import type { FrontendEvalTransport } from "../../../../core/frontendTransport"; import type { WindmillBackendSettings } from "../../../../core/windmillBackendSettings"; export interface ScriptEvalResult { @@ -33,8 +32,7 @@ export interface ScriptEvalOptions { model?: string; maxIterations?: number; provider?: AIProvider; - transport?: FrontendEvalTransport; - backend?: WindmillBackendSettings; + backend: WindmillBackendSettings; workspaceRoot?: string; runContext?: ModeRunContext; } @@ -98,10 +96,9 @@ export async function runScriptEval( model, workspace: workspaceRoot, provider: modelProvider.provider, - transport: options.transport, backend: options.backend, - proxyCaseId: options.runContext?.caseId, - proxyAttempt: options.runContext?.attempt, + caseId: options.runContext?.caseId, + attempt: options.runContext?.attempt, }, }); diff --git a/ai_evals/adapters/frontend/core/shared/baseEvalRunner.ts b/ai_evals/adapters/frontend/core/shared/baseEvalRunner.ts index 5b1f2e948e..7fd43ccb87 100644 --- a/ai_evals/adapters/frontend/core/shared/baseEvalRunner.ts +++ b/ai_evals/adapters/frontend/core/shared/baseEvalRunner.ts @@ -40,8 +40,8 @@ export interface RunEvalParams { apiKey: string; /** Function to get the current output state */ getOutput: () => TOutput; - /** Optional configuration */ - options?: EvalRunnerOptions; + /** Model and Windmill backend configuration */ + options: EvalRunnerOptions; onAssistantMessageStart?: () => void; onAssistantToken?: (token: string) => void; onAssistantMessageEnd?: () => void; @@ -70,10 +70,10 @@ export async function runEval( } = params; let shouldEmitMessageStart = true; - const model = options?.model ?? "gpt-4o"; - const maxIterations = options?.maxIterations ?? 20; - const workspace = options?.workspace ?? "test-workspace"; - const provider = toFrontendEvalProvider(options?.provider); + const model = options.model ?? "gpt-4o"; + const maxIterations = options.maxIterations ?? 20; + const workspace = options.workspace ?? "test-workspace"; + const provider = toFrontendEvalProvider(options.provider); const modelProvider = resolveEvalModelProvider(model, provider); @@ -203,45 +203,31 @@ export async function runEval( } }; - if (options?.transport === "proxy") { - const backendSettings = options.backend; - if (!backendSettings) { - throw new Error("Missing backend settings for proxy transport"); - } - - const backendClient = new WindmillBackendClient(backendSettings); - return await backendClient.withWorkspace( - options.proxyCaseId ?? "eval", - options.proxyAttempt ?? 1, - async (proxyWorkspaceId) => { - const resourcePath = buildProxyResourcePath(modelProvider.provider); - await backendClient.upsertResource({ - workspaceId: proxyWorkspaceId, - path: resourcePath, - resourceType: modelProvider.provider, - value: { api_key: apiKey }, - }); - const token = await backendClient.getToken(); - const clients = createEvalClients({ - provider: modelProvider.provider, - apiKey, - transport: "proxy", - proxy: { - baseURL: `${backendSettings.baseUrl}/api/w/${encodeURIComponent(proxyWorkspaceId)}/ai/proxy`, - bearerToken: token, - resourcePath, - }, - }) as unknown as ChatClients; - return await executeChatLoop(clients); - }, - ); - } - - const clients = createEvalClients({ - provider: modelProvider.provider, - apiKey, - }) as unknown as ChatClients; - return await executeChatLoop(clients); + const backendSettings = options.backend; + const backendClient = new WindmillBackendClient(backendSettings); + return await backendClient.withWorkspace( + options.caseId ?? "eval", + options.attempt ?? 1, + async (proxyWorkspaceId) => { + const resourcePath = buildProxyResourcePath(modelProvider.provider); + await backendClient.upsertResource({ + workspaceId: proxyWorkspaceId, + path: resourcePath, + resourceType: modelProvider.provider, + value: { api_key: apiKey }, + }); + const token = await backendClient.getToken(); + const clients = createEvalClients({ + provider: modelProvider.provider, + proxy: { + baseURL: `${backendSettings.baseUrl}/api/w/${encodeURIComponent(proxyWorkspaceId)}/ai/proxy`, + bearerToken: token, + resourcePath, + }, + }) as unknown as ChatClients; + return await executeChatLoop(clients); + }, + ); } function toFrontendEvalProvider( diff --git a/ai_evals/adapters/frontend/core/shared/providerConfig.test.ts b/ai_evals/adapters/frontend/core/shared/providerConfig.test.ts index e504ced376..77d9154da3 100644 --- a/ai_evals/adapters/frontend/core/shared/providerConfig.test.ts +++ b/ai_evals/adapters/frontend/core/shared/providerConfig.test.ts @@ -2,35 +2,9 @@ import { describe, expect, it } from "bun:test"; import { buildProxyHeaders, buildProxyResourcePath, - buildOpenAICompatibleClientOptions, resolveEvalModelProvider, } from "./providerConfig"; -describe("buildOpenAICompatibleClientOptions", () => { - it("adds Gemini's OpenAI-compatible base URL and client header", () => { - const options = buildOpenAICompatibleClientOptions( - "googleai", - "gemini-test-key", - ); - - expect(options).toMatchObject({ - apiKey: "gemini-test-key", - baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/", - defaultHeaders: { - "x-goog-api-client": "windmill-ai-evals/1.0", - }, - }); - }); - - it("keeps the default OpenAI-compatible config for OpenAI", () => { - expect( - buildOpenAICompatibleClientOptions("openai", "openai-test-key"), - ).toEqual({ - apiKey: "openai-test-key", - }); - }); -}); - describe("proxy helpers", () => { it("builds provider-scoped proxy resource paths", () => { expect(buildProxyResourcePath("googleai")).toBe("f/evals/ai/googleai"); diff --git a/ai_evals/adapters/frontend/core/shared/providerConfig.ts b/ai_evals/adapters/frontend/core/shared/providerConfig.ts index 62ba221a40..15372049fe 100644 --- a/ai_evals/adapters/frontend/core/shared/providerConfig.ts +++ b/ai_evals/adapters/frontend/core/shared/providerConfig.ts @@ -1,7 +1,6 @@ import Anthropic from "@anthropic-ai/sdk"; import OpenAI from "openai"; import type { FrontendEvalModelConfig } from "../../../../core/models"; -import type { FrontendEvalTransport } from "../../../../core/frontendTransport"; export type FrontendEvalProvider = FrontendEvalModelConfig["provider"]; @@ -15,15 +14,12 @@ export interface ResolvedEvalModelProvider { model: string; } -export interface EvalProxyClientConfig { +export interface WindmillAiProxyClientConfig { baseURL: string; bearerToken: string; resourcePath: string; } -const GEMINI_OPENAI_BASE_URL = - "https://generativelanguage.googleapis.com/v1beta/openai/"; -const GEMINI_GOOG_API_CLIENT = "windmill-ai-evals/1.0"; const EVAL_PROXY_RESOURCE_PREFIX = "f/evals/ai"; export function buildProxyHeaders( @@ -40,25 +36,8 @@ export function buildProxyResourcePath(provider: FrontendEvalProvider): string { return `${EVAL_PROXY_RESOURCE_PREFIX}/${provider}`; } -export function buildOpenAICompatibleClientOptions( - provider: Exclude, - apiKey: string, -): ConstructorParameters[0] { - if (provider === "googleai") { - return { - apiKey, - baseURL: GEMINI_OPENAI_BASE_URL, - defaultHeaders: { - "x-goog-api-client": GEMINI_GOOG_API_CLIENT, - }, - }; - } - - return { apiKey }; -} - function buildProxyOpenAIClientOptions( - proxy: EvalProxyClientConfig, + proxy: WindmillAiProxyClientConfig, ): ConstructorParameters[0] { return { apiKey: "unused", @@ -69,52 +48,24 @@ function buildProxyOpenAIClientOptions( export function createEvalClients(input: { provider: FrontendEvalProvider; - apiKey: string; - transport?: FrontendEvalTransport; - proxy?: EvalProxyClientConfig; + proxy: WindmillAiProxyClientConfig; }): EvalClients { - const transport = input.transport ?? "direct"; - if (input.provider === "anthropic") { - if (transport === "proxy") { - if (!input.proxy) { - throw new Error( - "Missing proxy client configuration for proxy transport", - ); - } - return { - openai: new OpenAI({ apiKey: "unused" }), - anthropic: new Anthropic({ - apiKey: "unused", - baseURL: input.proxy.baseURL, - defaultHeaders: buildProxyHeaders( - input.proxy.bearerToken, - input.proxy.resourcePath, - ), - }), - }; - } - return { openai: new OpenAI({ apiKey: "unused" }), - anthropic: new Anthropic({ apiKey: input.apiKey }), - }; - } - - if (transport === "proxy") { - if (!input.proxy) { - throw new Error("Missing proxy client configuration for proxy transport"); - } - return { - openai: new OpenAI(buildProxyOpenAIClientOptions(input.proxy)), - anthropic: new Anthropic({ apiKey: "unused" }), + anthropic: new Anthropic({ + apiKey: "unused", + baseURL: input.proxy.baseURL, + defaultHeaders: buildProxyHeaders( + input.proxy.bearerToken, + input.proxy.resourcePath, + ), + }), }; } return { - openai: new OpenAI( - buildOpenAICompatibleClientOptions(input.provider, input.apiKey), - ), + openai: new OpenAI(buildProxyOpenAIClientOptions(input.proxy)), anthropic: new Anthropic({ apiKey: "unused" }), }; } diff --git a/ai_evals/adapters/frontend/core/shared/types.ts b/ai_evals/adapters/frontend/core/shared/types.ts index f2a3f04794..f081fe398a 100644 --- a/ai_evals/adapters/frontend/core/shared/types.ts +++ b/ai_evals/adapters/frontend/core/shared/types.ts @@ -1,6 +1,5 @@ import type { ChatCompletionMessageParam } from "openai/resources/chat/completions.mjs"; import type { AIProvider } from "$lib/gen/types.gen"; -import type { FrontendEvalTransport } from "../../../../core/frontendTransport"; import type { WindmillBackendSettings } from "../../../../core/windmillBackendSettings"; export interface TokenUsage { @@ -15,14 +14,13 @@ export interface ToolCallDetail { } export interface EvalRunnerOptions { + backend: WindmillBackendSettings; maxIterations?: number; model?: string; workspace?: string; provider?: AIProvider; - transport?: FrontendEvalTransport; - backend?: WindmillBackendSettings; - proxyCaseId?: string; - proxyAttempt?: number; + caseId?: string; + attempt?: number; } export interface RawEvalResult { diff --git a/ai_evals/adapters/frontend/runtime.ts b/ai_evals/adapters/frontend/runtime.ts index 6a76fceeb0..180c7a2993 100644 --- a/ai_evals/adapters/frontend/runtime.ts +++ b/ai_evals/adapters/frontend/runtime.ts @@ -23,7 +23,6 @@ export async function runFrontendBenchmarkAdapter(input: { caseIds: string[]; runs: number; model?: string; - transport?: string; verbose?: boolean; backendValidation?: string; }): Promise { @@ -44,10 +43,6 @@ export async function runFrontendBenchmarkAdapter(input: { WMILL_FRONTEND_AI_EVAL_BACKEND_VALIDATION: input.backendValidation ?? "", }; - if (input.transport) { - env.WMILL_FRONTEND_AI_EVAL_TRANSPORT = input.transport; - } - try { await runVitestBenchmark( path.join(FRONTEND_DIR, "node_modules", ".bin", "vitest"), diff --git a/ai_evals/adapters/frontend/windmillBackend.test.ts b/ai_evals/adapters/frontend/windmillBackend.test.ts new file mode 100644 index 0000000000..302502c2d9 --- /dev/null +++ b/ai_evals/adapters/frontend/windmillBackend.test.ts @@ -0,0 +1,104 @@ +import { afterEach, describe, expect, it } from "bun:test"; +import type { WindmillBackendSettings } from "../../core/windmillBackendSettings"; +import { + WindmillBackendClient, + assertWindmillBackendReachable, +} from "./windmillBackend"; + +const ORIGINAL_FETCH = globalThis.fetch; + +afterEach(() => { + globalThis.fetch = ORIGINAL_FETCH; +}); + +describe("assertWindmillBackendReachable", () => { + it("logs in to verify backend reachability", async () => { + const requests: Array<{ url: string; init?: RequestInit }> = []; + globalThis.fetch = mockFetch(requests, textResponse(200, "token")); + + await expect( + assertWindmillBackendReachable( + buildSettings({ baseUrl: "http://backend.test/reachable" }), + ), + ).resolves.toBeUndefined(); + + expect(requests.map((entry) => entry.url)).toEqual([ + "http://backend.test/reachable/api/auth/login", + ]); + }); + + it("adds setup guidance when the backend cannot be initialized", async () => { + globalThis.fetch = mockFetch( + [], + textResponse(401, "invalid password"), + ); + + await expect( + assertWindmillBackendReachable( + buildSettings({ baseUrl: "http://backend.test/auth-failure" }), + ), + ).rejects.toThrow( + "Start a Windmill backend at that URL, or set WMILL_AI_EVAL_BACKEND_URL=.", + ); + }); +}); + +describe("WindmillBackendClient", () => { + it("creates or reuses the specified backend workspace without deleting it", async () => { + const requests: Array<{ url: string; init?: RequestInit }> = []; + globalThis.fetch = mockFetch( + requests, + textResponse(200, "token"), + textResponse(200, "false"), + textResponse(200, ""), + ); + + const client = new WindmillBackendClient( + buildSettings({ + baseUrl: "http://backend.test/shared-workspace", + workspaceOverride: "shared-evals", + }), + ); + + await expect( + client.withWorkspace("case-a", 1, async (workspaceId) => workspaceId), + ).resolves.toBe("shared-evals"); + + expect(requests.map((entry) => entry.url)).toEqual([ + "http://backend.test/shared-workspace/api/auth/login", + "http://backend.test/shared-workspace/api/workspaces/exists", + "http://backend.test/shared-workspace/api/workspaces/create", + ]); + }); +}); + +function buildSettings( + overrides: Partial = {}, +): WindmillBackendSettings { + return { + baseUrl: "http://backend.test/default", + email: "admin@windmill.dev", + password: "changeme", + ...overrides, + }; +} + +function mockFetch( + requests: Array<{ url: string; init?: RequestInit }>, + ...responses: Response[] +): typeof fetch { + const queue = [...responses]; + return async (input, init) => { + const url = String(input); + requests.push({ url, init }); + const next = queue.shift(); + if (!next) { + throw new Error(`Unexpected fetch: ${url}`); + } + return next; + }; +} + +function textResponse(status: number, body: string): Response { + return new Response(body, { status }); +} diff --git a/ai_evals/adapters/frontend/windmillBackend.ts b/ai_evals/adapters/frontend/windmillBackend.ts index c8d9537779..2247d8e5d5 100644 --- a/ai_evals/adapters/frontend/windmillBackend.ts +++ b/ai_evals/adapters/frontend/windmillBackend.ts @@ -3,6 +3,7 @@ import type { WindmillBackendSettings } from "../../core/windmillBackendSettings const tokenCache = new Map>(); const sharedWorkspaceQueue = new Map>(); +const DEFAULT_WORKSPACE_PREFIX = "ai-evals"; export class WindmillBackendClient { constructor(private readonly settings: WindmillBackendSettings) {} @@ -14,7 +15,7 @@ export class WindmillBackendClient { ): Promise { const workspaceId = this.settings.workspaceOverride ?? - buildWorkspaceId(this.settings.workspacePrefix, caseId, attempt); + buildWorkspaceId(caseId, attempt); const run = async () => { await this.ensureWorkspace(workspaceId); @@ -22,7 +23,7 @@ export class WindmillBackendClient { try { return await body(workspaceId); } finally { - if (!this.settings.keepWorkspaces && !this.settings.workspaceOverride) { + if (!this.settings.workspaceOverride) { await this.deleteWorkspace(workspaceId).catch(() => undefined); } } @@ -136,6 +137,24 @@ export class WindmillBackendClient { } } +export async function assertWindmillBackendReachable( + settings: WindmillBackendSettings, +): Promise { + try { + await new WindmillBackendClient(settings).getToken(); + } catch (error) { + const details = error instanceof Error ? error.message : String(error); + throw new Error( + [ + `Could not initialize the Windmill backend for AI eval proxy at ${settings.baseUrl}.`, + "Start a Windmill backend at that URL, or set WMILL_AI_EVAL_BACKEND_URL=.", + `Using login ${settings.email}; if authentication failed, set WMILL_AI_EVAL_BACKEND_EMAIL and WMILL_AI_EVAL_BACKEND_PASSWORD.`, + `Details: ${details}`, + ].join("\n"), + ); + } +} + async function withSharedWorkspaceLock( workspaceId: string, body: () => Promise, @@ -160,18 +179,14 @@ async function withSharedWorkspaceLock( } } -function buildWorkspaceId( - prefix: string, - caseId: string, - attempt: number, -): string { +function buildWorkspaceId(caseId: string, attempt: number): string { const caseSlug = caseId .toLowerCase() .replace(/[^a-z0-9-]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 30); const suffix = randomUUID().slice(0, 8); - return `${prefix}-${caseSlug || "case"}-a${attempt}-${suffix}`; + return `${DEFAULT_WORKSPACE_PREFIX}-${caseSlug || "case"}-a${attempt}-${suffix}`; } async function expectOk(response: Response, context: string): Promise { diff --git a/ai_evals/cli/index.ts b/ai_evals/cli/index.ts index 202d826078..259b055ff5 100644 --- a/ai_evals/cli/index.ts +++ b/ai_evals/cli/index.ts @@ -27,11 +27,8 @@ import { EVAL_MODES, type EvalMode } from "../core/types"; import { DEFAULT_JUDGE_MODEL } from "../core/judge"; import { createCliModeRunner } from "../modes/cli"; import { runFrontendBenchmarkAdapter } from "../adapters/frontend/runtime"; -import { - FRONTEND_EVAL_TRANSPORTS, - type FrontendEvalTransport, - parseFrontendEvalTransport, -} from "../core/frontendTransport"; +import { resolveWindmillBackendSettings } from "../core/windmillBackendSettings"; +import { assertWindmillBackendReachable } from "../adapters/frontend/windmillBackend"; async function main() { const program = new Command() @@ -98,10 +95,6 @@ async function main() { "--models ", "comma-separated model aliases to run sequentially", ) - .option( - "--transport ", - `frontend transport (${FRONTEND_EVAL_TRANSPORTS.join(", ")})`, - ) .option("--verbose", "stream assistant output during frontend runs") .option( "--record", @@ -120,7 +113,6 @@ async function main() { output?: string; model?: string; models?: string; - transport?: string; verbose?: boolean; record?: boolean; backendValidation?: string; @@ -133,9 +125,6 @@ async function main() { outputPath: options.output, model: options.model, models: options.models, - transport: options.transport - ? parseFrontendEvalTransport(options.transport) - : undefined, verbose: options.verbose ?? false, record: options.record ?? false, backendValidation: options.backendValidation, @@ -184,7 +173,6 @@ async function handleRun(input: { outputPath?: string; model?: string; models?: string; - transport?: FrontendEvalTransport; verbose: boolean; record: boolean; backendValidation?: string; @@ -197,11 +185,6 @@ async function handleRun(input: { if (input.model && input.models) { throw new Error("Use either --model or --models, not both"); } - if (input.mode === "cli" && input.transport === "proxy") { - throw new Error( - "--transport proxy is only supported for flow, script, and app modes", - ); - } const selectedCases = await loadSelectedCases(input.mode, input.caseIds); const models = resolveRequestedModels(input.mode, input.model, input.models); @@ -220,6 +203,9 @@ async function handleRun(input: { "--backend-validation currently supports only flow and script modes", ); } + if (input.mode !== "cli") { + await assertWindmillBackendReachable(resolveWindmillBackendSettings()); + } const summaries: Array<{ label: string; @@ -249,7 +235,6 @@ async function handleRun(input: { caseIds: input.caseIds, runs: input.runs, model: model.id, - transport: input.transport, verbose: input.verbose, backendValidation, }); diff --git a/ai_evals/core/backendValidation.ts b/ai_evals/core/backendValidation.ts index 87094fd23f..1484aee2d4 100644 --- a/ai_evals/core/backendValidation.ts +++ b/ai_evals/core/backendValidation.ts @@ -13,9 +13,7 @@ export interface BackendValidationSettings { baseUrl: string; email: string; password: string; - keepWorkspaces: boolean; workspaceOverride?: string; - workspacePrefix: string; pollIntervalMs: number; maxWaitMs: number; } diff --git a/ai_evals/core/frontendTransport.test.ts b/ai_evals/core/frontendTransport.test.ts deleted file mode 100644 index 09ebd1b3a7..0000000000 --- a/ai_evals/core/frontendTransport.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { afterEach, describe, expect, it } from "bun:test"; -import { - parseFrontendEvalTransport, - resolveFrontendEvalTransportSettings, -} from "./frontendTransport"; - -const ORIGINAL_ENV = { - WMILL_AI_EVAL_BACKEND_URL: process.env.WMILL_AI_EVAL_BACKEND_URL, -}; - -afterEach(() => { - if (ORIGINAL_ENV.WMILL_AI_EVAL_BACKEND_URL === undefined) { - delete process.env.WMILL_AI_EVAL_BACKEND_URL; - } else { - process.env.WMILL_AI_EVAL_BACKEND_URL = - ORIGINAL_ENV.WMILL_AI_EVAL_BACKEND_URL; - } -}); - -describe("parseFrontendEvalTransport", () => { - it("defaults to direct when unset", () => { - expect(parseFrontendEvalTransport(undefined)).toBe("direct"); - }); - - it("accepts proxy explicitly", () => { - expect(parseFrontendEvalTransport("proxy")).toBe("proxy"); - }); - - it("rejects unsupported values", () => { - expect(() => parseFrontendEvalTransport("worker")).toThrow( - "Unsupported frontend eval transport: worker", - ); - }); -}); - -describe("resolveFrontendEvalTransportSettings", () => { - it("includes backend settings for proxy transport", () => { - process.env.WMILL_AI_EVAL_BACKEND_URL = "http://127.0.0.1:8000/"; - - expect( - resolveFrontendEvalTransportSettings({ - evalMode: "app", - requestedTransport: "proxy", - }), - ).toMatchObject({ - transport: "proxy", - backend: { - baseUrl: "http://127.0.0.1:8000", - }, - }); - }); - - it("keeps direct transport for cli runs", () => { - expect( - resolveFrontendEvalTransportSettings({ - evalMode: "cli", - requestedTransport: "direct", - }), - ).toEqual({ - transport: "direct", - backend: undefined, - }); - }); -}); diff --git a/ai_evals/core/frontendTransport.ts b/ai_evals/core/frontendTransport.ts deleted file mode 100644 index fa78505113..0000000000 --- a/ai_evals/core/frontendTransport.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { EvalMode } from "./types"; -import type { WindmillBackendSettings } from "./windmillBackendSettings"; -import { resolveWindmillBackendSettings } from "./windmillBackendSettings"; - -export const FRONTEND_EVAL_TRANSPORTS = ["direct", "proxy"] as const; - -export type FrontendEvalTransport = (typeof FRONTEND_EVAL_TRANSPORTS)[number]; - -export interface FrontendEvalTransportSettings { - transport: FrontendEvalTransport; - backend?: WindmillBackendSettings; -} - -export function parseFrontendEvalTransport( - value?: string | null, -): FrontendEvalTransport { - const normalized = value?.trim().toLowerCase(); - - if (!normalized || normalized === "direct") { - return "direct"; - } - - if (normalized === "proxy") { - return "proxy"; - } - - throw new Error( - `Unsupported frontend eval transport: ${value}. Use one of: ${FRONTEND_EVAL_TRANSPORTS.join(", ")}`, - ); -} - -export function resolveFrontendEvalTransportSettings(input: { - evalMode: EvalMode; - requestedTransport?: string | null; -}): FrontendEvalTransportSettings { - const transport = parseFrontendEvalTransport(input.requestedTransport); - - if (transport === "proxy" && input.evalMode === "cli") { - throw new Error( - 'Frontend eval transport "proxy" is only supported for flow, script, and app evals', - ); - } - - return { - transport, - backend: - transport === "proxy" ? resolveWindmillBackendSettings() : undefined, - }; -} diff --git a/ai_evals/core/results.ts b/ai_evals/core/results.ts index c5f6b8749b..e58840f911 100644 --- a/ai_evals/core/results.ts +++ b/ai_evals/core/results.ts @@ -74,7 +74,6 @@ export function buildRunResult(input: { mode: EvalMode; runs: number; runModel: string | null; - transport?: BenchmarkRunResult["transport"]; judgeModel: string | null; caseResults: BenchmarkCaseResult[]; }): BenchmarkRunResult { @@ -116,7 +115,6 @@ export function buildRunResult(input: { gitSha: getGitSha(), runs: input.runs, runModel: input.runModel, - transport: input.transport ?? null, judgeModel: input.judgeModel, caseCount: input.caseResults.length, attemptCount, @@ -142,9 +140,6 @@ export function formatRunSummary(result: BenchmarkRunResult): string { `Pass rate: ${formatPercent(result.passRate)} (${result.passedAttempts}/${result.attemptCount})`, `Average duration: ${Math.round(result.averageDurationMs)}ms`, ]; - if (result.transport) { - lines.splice(1, 0, `Transport: ${result.transport}`); - } const failures = collectFailures(result); if (failures.length > 0) { @@ -251,7 +246,6 @@ function toHistoryRecord(result: BenchmarkRunResult) { mode: result.mode, runs: result.runs, runModel: result.runModel, - transport: result.transport, judgeModel: result.judgeModel, caseCount: result.caseCount, attemptCount: result.attemptCount, diff --git a/ai_evals/core/types.ts b/ai_evals/core/types.ts index 2d612be21a..dfc6882f84 100644 --- a/ai_evals/core/types.ts +++ b/ai_evals/core/types.ts @@ -1,7 +1,6 @@ export const EVAL_MODES = ["cli", "flow", "script", "app"] as const; export type EvalMode = (typeof EVAL_MODES)[number]; -export type FrontendEvalTransport = "direct" | "proxy"; export interface EvalCaseRuntimeBackendPreview { args?: Record; @@ -297,7 +296,6 @@ export interface BenchmarkRunResult { gitSha: string | null; runs: number; runModel: string | null; - transport: FrontendEvalTransport | null; judgeModel: string | null; caseCount: number; attemptCount: number; diff --git a/ai_evals/core/windmillBackendSettings.test.ts b/ai_evals/core/windmillBackendSettings.test.ts new file mode 100644 index 0000000000..200bc4aa7a --- /dev/null +++ b/ai_evals/core/windmillBackendSettings.test.ts @@ -0,0 +1,63 @@ +import { afterEach, describe, expect, it } from "bun:test"; +import { resolveWindmillBackendSettings } from "./windmillBackendSettings"; + +const ENV_KEYS = [ + "WMILL_AI_EVAL_BACKEND_URL", + "WINDMILL_URL", + "WINDMILL_BASE_URL", + "REMOTE", + "WMILL_AI_EVAL_BACKEND_EMAIL", + "WMILL_AI_EVAL_BACKEND_PASSWORD", + "WMILL_AI_EVAL_BACKEND_WORKSPACE", +] as const; + +const ORIGINAL_ENV = Object.fromEntries( + ENV_KEYS.map((key) => [key, process.env[key]]), +) as Record<(typeof ENV_KEYS)[number], string | undefined>; + +afterEach(() => { + for (const key of ENV_KEYS) { + const value = ORIGINAL_ENV[key]; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } +}); + +describe("resolveWindmillBackendSettings", () => { + it("uses backend URL/auth defaults and the optional explicit workspace", () => { + delete process.env.WMILL_AI_EVAL_BACKEND_URL; + delete process.env.WINDMILL_URL; + delete process.env.WINDMILL_BASE_URL; + delete process.env.REMOTE; + process.env.WMILL_AI_EVAL_BACKEND_WORKSPACE = "shared-evals"; + + expect(resolveWindmillBackendSettings()).toEqual({ + baseUrl: "http://127.0.0.1:8000", + email: "admin@windmill.dev", + password: "changeme", + workspaceOverride: "shared-evals", + }); + }); + + it("does not expose workspace retention knobs", () => { + process.env.WMILL_AI_EVAL_BACKEND_URL = "http://backend.test/"; + + const settings = resolveWindmillBackendSettings(); + + expect(settings).toEqual({ + baseUrl: "http://backend.test", + email: "admin@windmill.dev", + password: "changeme", + workspaceOverride: undefined, + }); + expect(Object.keys(settings).sort()).toEqual([ + "baseUrl", + "email", + "password", + "workspaceOverride", + ]); + }); +}); diff --git a/ai_evals/core/windmillBackendSettings.ts b/ai_evals/core/windmillBackendSettings.ts index c3a0a52d47..2388c32b4d 100644 --- a/ai_evals/core/windmillBackendSettings.ts +++ b/ai_evals/core/windmillBackendSettings.ts @@ -2,9 +2,7 @@ export interface WindmillBackendSettings { baseUrl: string; email: string; password: string; - keepWorkspaces: boolean; workspaceOverride?: string; - workspacePrefix: string; } export function resolveWindmillBackendSettings(): WindmillBackendSettings { @@ -18,13 +16,9 @@ export function resolveWindmillBackendSettings(): WindmillBackendSettings { ), email: process.env.WMILL_AI_EVAL_BACKEND_EMAIL ?? "admin@windmill.dev", password: process.env.WMILL_AI_EVAL_BACKEND_PASSWORD ?? "changeme", - keepWorkspaces: isTruthy(process.env.WMILL_AI_EVAL_KEEP_WORKSPACES), workspaceOverride: sanitizeOptionalWorkspaceId( process.env.WMILL_AI_EVAL_BACKEND_WORKSPACE, ), - workspacePrefix: sanitizeWorkspacePrefix( - process.env.WMILL_AI_EVAL_WORKSPACE_PREFIX ?? "ai-evals", - ), }; } @@ -43,25 +37,9 @@ function normalizeBaseUrl(value: string): string { return value.replace(/\/+$/, ""); } -function sanitizeWorkspacePrefix(value: string): string { - const sanitized = value - .trim() - .toLowerCase() - .replace(/[^a-z0-9-]+/g, "-") - .replace(/^-+|-+$/g, ""); - return sanitized.length > 0 ? sanitized : "ai-evals"; -} - function sanitizeOptionalWorkspaceId( value: string | undefined, ): string | undefined { const trimmed = value?.trim(); return trimmed ? trimmed : undefined; } - -function isTruthy(value: string | undefined): boolean { - if (!value) { - return false; - } - return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase()); -} diff --git a/ai_evals/modes/app.ts b/ai_evals/modes/app.ts index 5bca0ad878..af9d7c667b 100644 --- a/ai_evals/modes/app.ts +++ b/ai_evals/modes/app.ts @@ -5,15 +5,12 @@ import type { FrontendEvalModelConfig } from "../core/models"; import { validateAppState, type AppFilesState } from "../core/validators"; import type { BenchmarkArtifactFile, ModeRunner } from "../core/types"; import { runAppEval } from "../adapters/frontend/core/app/appEvalRunner"; -import { - DEFAULT_FRONTEND_EVAL_MODEL, - getFrontendApiKey, -} from "./frontendCommon"; -import type { FrontendEvalTransportSettings } from "../core/frontendTransport"; +import { getFrontendApiKey } from "./frontendCommon"; +import type { WindmillBackendSettings } from "../core/windmillBackendSettings"; export function createAppModeRunner( - modelConfig: FrontendEvalModelConfig = DEFAULT_FRONTEND_EVAL_MODEL, - transportSettings?: FrontendEvalTransportSettings, + modelConfig: FrontendEvalModelConfig, + backendSettings: WindmillBackendSettings, ): ModeRunner { return { mode: "app", @@ -37,8 +34,7 @@ export function createAppModeRunner( appContext: context.evalCase?.runtime?.appContext, provider: modelConfig.provider, model: modelConfig.model, - transport: transportSettings?.transport, - backend: transportSettings?.backend, + backend: backendSettings, runContext: context, }, ); diff --git a/ai_evals/modes/flow.ts b/ai_evals/modes/flow.ts index 4e4f6451b9..e40a495573 100644 --- a/ai_evals/modes/flow.ts +++ b/ai_evals/modes/flow.ts @@ -7,11 +7,8 @@ import type { BenchmarkArtifactFile, ModeRunner } from "../core/types"; import { runFlowEval } from "../adapters/frontend/core/flow/flowEvalRunner"; import type { FlowWorkspaceFixtures } from "../adapters/frontend/core/flow/fileHelpers"; import { BackendPreviewClient } from "../adapters/frontend/backendPreview"; -import { - DEFAULT_FRONTEND_EVAL_MODEL, - getFrontendApiKey, -} from "./frontendCommon"; -import type { FrontendEvalTransportSettings } from "../core/frontendTransport"; +import { getFrontendApiKey } from "./frontendCommon"; +import type { WindmillBackendSettings } from "../core/windmillBackendSettings"; import { normalizeFlowInitialFixture, normalizeFlowStateFixture, @@ -19,9 +16,9 @@ import { } from "./flowFixtures"; export function createFlowModeRunner( - modelConfig: FrontendEvalModelConfig = DEFAULT_FRONTEND_EVAL_MODEL, - backendValidation?: BackendValidationSettings, - transportSettings?: FrontendEvalTransportSettings, + modelConfig: FrontendEvalModelConfig, + backendValidation: BackendValidationSettings | undefined, + backendSettings: WindmillBackendSettings, ): ModeRunner { return { mode: "flow", @@ -49,8 +46,7 @@ export function createFlowModeRunner( maxIterations: context.evalCase?.runtime?.maxTurns, provider: modelConfig.provider, model: modelConfig.model, - transport: transportSettings?.transport, - backend: transportSettings?.backend, + backend: backendSettings, runContext: context, }, ); diff --git a/ai_evals/modes/frontendCommon.ts b/ai_evals/modes/frontendCommon.ts index 2619d21821..f121551d86 100644 --- a/ai_evals/modes/frontendCommon.ts +++ b/ai_evals/modes/frontendCommon.ts @@ -1,12 +1,4 @@ -import { - getFrontendEvalModel, - resolveEvalModel, - type FrontendEvalModelConfig, -} from "../core/models"; - -export const DEFAULT_FRONTEND_EVAL_MODEL: FrontendEvalModelConfig = getFrontendEvalModel( - resolveEvalModel("flow") -); +import type { FrontendEvalModelConfig } from "../core/models"; export function getFrontendApiKey(provider: FrontendEvalModelConfig["provider"]): string { const envName = diff --git a/ai_evals/modes/script.ts b/ai_evals/modes/script.ts index 7671e8220a..0c49b05d7d 100644 --- a/ai_evals/modes/script.ts +++ b/ai_evals/modes/script.ts @@ -6,16 +6,13 @@ import type { BenchmarkArtifactFile, ModeRunner } from "../core/types"; import { BackendPreviewClient } from "../adapters/frontend/backendPreview"; import { runScriptEval } from "../adapters/frontend/core/script/scriptEvalRunner"; import type { ScriptEvalState } from "../adapters/frontend/core/script/fileHelpers"; -import { - DEFAULT_FRONTEND_EVAL_MODEL, - getFrontendApiKey, -} from "./frontendCommon"; -import type { FrontendEvalTransportSettings } from "../core/frontendTransport"; +import { getFrontendApiKey } from "./frontendCommon"; +import type { WindmillBackendSettings } from "../core/windmillBackendSettings"; export function createScriptModeRunner( - modelConfig: FrontendEvalModelConfig = DEFAULT_FRONTEND_EVAL_MODEL, - backendValidation?: BackendValidationSettings, - transportSettings?: FrontendEvalTransportSettings, + modelConfig: FrontendEvalModelConfig, + backendValidation: BackendValidationSettings | undefined, + backendSettings: WindmillBackendSettings, ): ModeRunner { return { mode: "script", @@ -40,8 +37,7 @@ export function createScriptModeRunner( maxIterations: context.evalCase?.runtime?.maxTurns, provider: modelConfig.provider, model: modelConfig.model, - transport: transportSettings?.transport, - backend: transportSettings?.backend, + backend: backendSettings, runContext: context, }, ); diff --git a/backend/.sqlx/query-8b92a7d04fcdd8e61178d7dab97c31e10f89481908c479b4039af5e94fa0f8ac.json b/backend/.sqlx/query-290599fc173947acb518344d6fb631af9f524389309f17ef04f70c773b1d5e75.json similarity index 85% rename from backend/.sqlx/query-8b92a7d04fcdd8e61178d7dab97c31e10f89481908c479b4039af5e94fa0f8ac.json rename to backend/.sqlx/query-290599fc173947acb518344d6fb631af9f524389309f17ef04f70c773b1d5e75.json index 415544ece9..ad296e9eda 100644 --- a/backend/.sqlx/query-8b92a7d04fcdd8e61178d7dab97c31e10f89481908c479b4039af5e94fa0f8ac.json +++ b/backend/.sqlx/query-290599fc173947acb518344d6fb631af9f524389309f17ef04f70c773b1d5e75.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT s.item_kind, s.path\n FROM ws_specific s\n WHERE s.workspace_id = $1\n AND (\n (s.item_kind = 'resource' AND EXISTS (\n SELECT 1 FROM resource r\n WHERE r.workspace_id = s.workspace_id AND r.path = s.path\n ))\n OR (s.item_kind = 'variable' AND EXISTS (\n SELECT 1 FROM variable v\n WHERE v.workspace_id = s.workspace_id AND v.path = s.path\n ))\n )\n ", + "query": "\n SELECT s.item_kind, s.path\n FROM ws_specific s\n WHERE s.workspace_id = $1\n AND (\n (s.item_kind = 'resource' AND EXISTS (\n SELECT 1 FROM resource r\n WHERE r.workspace_id = s.workspace_id AND r.path = s.path\n ))\n OR (s.item_kind = 'variable' AND EXISTS (\n SELECT 1 FROM variable v\n WHERE v.workspace_id = s.workspace_id AND v.path = s.path\n ))\n )\n ORDER BY s.item_kind, s.path\n ", "describe": { "columns": [ { @@ -24,5 +24,5 @@ false ] }, - "hash": "8b92a7d04fcdd8e61178d7dab97c31e10f89481908c479b4039af5e94fa0f8ac" + "hash": "290599fc173947acb518344d6fb631af9f524389309f17ef04f70c773b1d5e75" } diff --git a/backend/.sqlx/query-6474dbc070afdce3e6bc74b02f93d94c27fcbbf989a4a70678dbc54e0d896e24.json b/backend/.sqlx/query-6474dbc070afdce3e6bc74b02f93d94c27fcbbf989a4a70678dbc54e0d896e24.json new file mode 100644 index 0000000000..702e6e36bb --- /dev/null +++ b/backend/.sqlx/query-6474dbc070afdce3e6bc74b02f93d94c27fcbbf989a4a70678dbc54e0d896e24.json @@ -0,0 +1,41 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT service_config, webhook_token_hash\n FROM native_trigger\n WHERE workspace_id = $1\n AND service_name = $2\n AND external_id = $3\n FOR UPDATE SKIP LOCKED\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "service_config", + "type_info": "Jsonb" + }, + { + "ordinal": 1, + "name": "webhook_token_hash", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text", + { + "Custom": { + "name": "native_trigger_service", + "kind": { + "Enum": [ + "nextcloud", + "google", + "github" + ] + } + } + }, + "Text" + ] + }, + "nullable": [ + true, + false + ] + }, + "hash": "6474dbc070afdce3e6bc74b02f93d94c27fcbbf989a4a70678dbc54e0d896e24" +} diff --git a/backend/.sqlx/query-406bcbf55758b10243c8eaff1c349b8082c0052d626bf67e08317e56ab9ad026.json b/backend/.sqlx/query-676405f5ba49c1c711646bd0882fb888d53c5e4aa53bfc43ca296863bc61813f.json similarity index 62% rename from backend/.sqlx/query-406bcbf55758b10243c8eaff1c349b8082c0052d626bf67e08317e56ab9ad026.json rename to backend/.sqlx/query-676405f5ba49c1c711646bd0882fb888d53c5e4aa53bfc43ca296863bc61813f.json index 2b5b68dfae..a1aa4cc015 100644 --- a/backend/.sqlx/query-406bcbf55758b10243c8eaff1c349b8082c0052d626bf67e08317e56ab9ad026.json +++ b/backend/.sqlx/query-676405f5ba49c1c711646bd0882fb888d53c5e4aa53bfc43ca296863bc61813f.json @@ -1,42 +1,32 @@ { "db_name": "PostgreSQL", - "query": "SELECT label, email, scopes, workspace_id, super_admin, owner, expiration FROM token WHERE token_hash = $1", + "query": "SELECT email, scopes, workspace_id, super_admin, owner FROM token WHERE token_hash = $1", "describe": { "columns": [ { "ordinal": 0, - "name": "label", - "type_info": "Varchar" - }, - { - "ordinal": 1, "name": "email", "type_info": "Varchar" }, { - "ordinal": 2, + "ordinal": 1, "name": "scopes", "type_info": "TextArray" }, { - "ordinal": 3, + "ordinal": 2, "name": "workspace_id", "type_info": "Varchar" }, { - "ordinal": 4, + "ordinal": 3, "name": "super_admin", "type_info": "Bool" }, { - "ordinal": 5, + "ordinal": 4, "name": "owner", "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "expiration", - "type_info": "Timestamptz" } ], "parameters": { @@ -48,11 +38,9 @@ true, true, true, - true, false, - true, true ] }, - "hash": "406bcbf55758b10243c8eaff1c349b8082c0052d626bf67e08317e56ab9ad026" + "hash": "676405f5ba49c1c711646bd0882fb888d53c5e4aa53bfc43ca296863bc61813f" } diff --git a/backend/.sqlx/query-6be4bf59c404d2f557d1106c48c320bb3eff65255a44bd66799ae14288312ba4.json b/backend/.sqlx/query-6be4bf59c404d2f557d1106c48c320bb3eff65255a44bd66799ae14288312ba4.json deleted file mode 100644 index 0a39db6822..0000000000 --- a/backend/.sqlx/query-6be4bf59c404d2f557d1106c48c320bb3eff65255a44bd66799ae14288312ba4.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT EXISTS(SELECT 1 FROM variable WHERE workspace_id = $1 AND path = $2)", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "exists", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - null - ] - }, - "hash": "6be4bf59c404d2f557d1106c48c320bb3eff65255a44bd66799ae14288312ba4" -} diff --git a/backend/.sqlx/query-9c85ba8d41bedbcb5466f44a7d4cf6b4946e1fd337f00d243f518283783833c9.json b/backend/.sqlx/query-9c85ba8d41bedbcb5466f44a7d4cf6b4946e1fd337f00d243f518283783833c9.json new file mode 100644 index 0000000000..fce125c6d5 --- /dev/null +++ b/backend/.sqlx/query-9c85ba8d41bedbcb5466f44a7d4cf6b4946e1fd337f00d243f518283783833c9.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT bool_and(operator) FROM (\n SELECT operator FROM usr WHERE email = $1\n UNION ALL\n SELECT operator FROM workspace_invite WHERE email = $1\n ) t", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "bool_and", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "9c85ba8d41bedbcb5466f44a7d4cf6b4946e1fd337f00d243f518283783833c9" +} diff --git a/backend/.sqlx/query-0c6e8f03a4e9f543cb85582e0aec1ed508d83695ef6d62ca06cfb612fd332b87.json b/backend/.sqlx/query-b02d3563b72ee8989dea128a3c6a2be62ce1f8f7edf3793d7ad4af9eb11b648f.json similarity index 50% rename from backend/.sqlx/query-0c6e8f03a4e9f543cb85582e0aec1ed508d83695ef6d62ca06cfb612fd332b87.json rename to backend/.sqlx/query-b02d3563b72ee8989dea128a3c6a2be62ce1f8f7edf3793d7ad4af9eb11b648f.json index 7de0416a12..3536567e16 100644 --- a/backend/.sqlx/query-0c6e8f03a4e9f543cb85582e0aec1ed508d83695ef6d62ca06cfb612fd332b87.json +++ b/backend/.sqlx/query-b02d3563b72ee8989dea128a3c6a2be62ce1f8f7edf3793d7ad4af9eb11b648f.json @@ -1,17 +1,17 @@ { "db_name": "PostgreSQL", - "query": "SELECT item_kind, path FROM ws_specific WHERE workspace_id = $1", + "query": "SELECT label, expiration FROM token WHERE token_hash = $1", "describe": { "columns": [ { "ordinal": 0, - "name": "item_kind", + "name": "label", "type_info": "Varchar" }, { "ordinal": 1, - "name": "path", - "type_info": "Varchar" + "name": "expiration", + "type_info": "Timestamptz" } ], "parameters": { @@ -20,9 +20,9 @@ ] }, "nullable": [ - false, - false + true, + true ] }, - "hash": "0c6e8f03a4e9f543cb85582e0aec1ed508d83695ef6d62ca06cfb612fd332b87" + "hash": "b02d3563b72ee8989dea128a3c6a2be62ce1f8f7edf3793d7ad4af9eb11b648f" } diff --git a/backend/.sqlx/query-b4162468afae99cf31c4668ca6769657fd73742b6ee8289b1e9736e381314cfb.json b/backend/.sqlx/query-b4162468afae99cf31c4668ca6769657fd73742b6ee8289b1e9736e381314cfb.json deleted file mode 100644 index 327032afb5..0000000000 --- a/backend/.sqlx/query-b4162468afae99cf31c4668ca6769657fd73742b6ee8289b1e9736e381314cfb.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT EXISTS(SELECT 1 FROM ws_specific WHERE workspace_id = $1 AND item_kind = 'variable' AND path = $2)", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "exists", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - null - ] - }, - "hash": "b4162468afae99cf31c4668ca6769657fd73742b6ee8289b1e9736e381314cfb" -} diff --git a/backend/.sqlx/query-e1ada31c1625b453c2ff85edbcd7ad51a4cd5cbdc2fa34038530070d6a579455.json b/backend/.sqlx/query-e1ada31c1625b453c2ff85edbcd7ad51a4cd5cbdc2fa34038530070d6a579455.json new file mode 100644 index 0000000000..f38c023cb3 --- /dev/null +++ b/backend/.sqlx/query-e1ada31c1625b453c2ff85edbcd7ad51a4cd5cbdc2fa34038530070d6a579455.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "WITH potential AS (\n SELECT email, operator FROM usr\n UNION\n SELECT email, operator FROM workspace_invite\n ),\n per_user AS (\n SELECT email, bool_and(operator) AS only_operator FROM potential GROUP BY email\n )\n SELECT\n COUNT(*) FILTER (WHERE NOT only_operator) AS \"authors!\",\n COUNT(*) FILTER (WHERE only_operator) AS \"operators!\"\n FROM per_user", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "authors!", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "operators!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null, + null + ] + }, + "hash": "e1ada31c1625b453c2ff85edbcd7ad51a4cd5cbdc2fa34038530070d6a579455" +} diff --git a/backend/.sqlx/query-f8f756bc498e5f084851f98e1e8d8c74cdc672a903d566baf5ac5ef50a4da1bd.json b/backend/.sqlx/query-f8f756bc498e5f084851f98e1e8d8c74cdc672a903d566baf5ac5ef50a4da1bd.json new file mode 100644 index 0000000000..e4125ddab9 --- /dev/null +++ b/backend/.sqlx/query-f8f756bc498e5f084851f98e1e8d8c74cdc672a903d566baf5ac5ef50a4da1bd.json @@ -0,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT memory, worker, native_mode FROM worker_ping WHERE ping_at > now() - interval '2 minutes'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "memory", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "worker", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "native_mode", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + true, + false, + false + ] + }, + "hash": "f8f756bc498e5f084851f98e1e8d8c74cdc672a903d566baf5ac5ef50a4da1bd" +} diff --git a/backend/Cargo.lock b/backend/Cargo.lock index c6bf144004..00e8959f72 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -2,16 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" -dependencies = [ - "lazy_static", - "regex", -] - [[package]] name = "addr2line" version = "0.25.1" @@ -21,12 +11,6 @@ dependencies = [ "gimli", ] -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "adler2" version = "2.0.1" @@ -39,24 +23,10 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common 0.1.7", + "crypto-common", "generic-array", ] -[[package]] -name = "aead-gcm-stream" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70c8dec860340effb00f6945c49c0daaa6dac963602750db862eabb74bf7886" -dependencies = [ - "aead", - "aes 0.8.3", - "cipher 0.4.4", - "ctr", - "ghash", - "subtle", -] - [[package]] name = "aes" version = "0.7.5" @@ -71,9 +41,9 @@ dependencies = [ [[package]] name = "aes" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher 0.4.4", @@ -87,22 +57,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ "aead", - "aes 0.8.3", + "aes 0.8.4", "cipher 0.4.4", "ctr", "ghash", "subtle", ] -[[package]] -name = "aes-kw" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69fa2b352dcefb5f7f3a5fb840e02665d311d878955380515e4fd50095dd3d8c" -dependencies = [ - "aes 0.8.3", -] - [[package]] name = "ahash" version = "0.7.8" @@ -279,9 +240,6 @@ name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -dependencies = [ - "serde", -] [[package]] name = "arrow" @@ -494,29 +452,10 @@ dependencies = [ ] [[package]] -name = "ash" -version = "0.37.3+1.3.251" +name = "ascii" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a" -dependencies = [ - "libloading 0.7.4", -] - -[[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 7.1.3", - "num-traits", - "rusticata-macros", - "thiserror 1.0.69", - "time", -] +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" [[package]] name = "asn1-rs" @@ -524,28 +463,16 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" dependencies = [ - "asn1-rs-derive 0.5.1", - "asn1-rs-impl 0.2.0", + "asn1-rs-derive", + "asn1-rs-impl", "displaydoc", - "nom 7.1.3", + "nom", "num-traits", "rusticata-macros", "thiserror 1.0.69", "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" @@ -555,18 +482,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.117", - "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", + "synstructure", ] [[package]] @@ -582,11 +498,10 @@ dependencies = [ [[package]] name = "ast_node" -version = "0.9.9" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9184f2b369b3e8625712493c89b785881f27eedc6cde480a81883cef78868b2" +checksum = "0a184645bcc6f52d69d8e7639720699c6a99efb711f886e251ed1d16db8dd90e" dependencies = [ - "proc-macro2", "quote", "swc_macros_common", "syn 2.0.117", @@ -1137,7 +1052,7 @@ dependencies = [ "bytes", "form_urlencoded", "hex", - "hmac 0.12.1", + "hmac", "http 0.2.12", "http 1.4.0", "percent-encoding", @@ -1326,7 +1241,7 @@ version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "576b0d6991c9c32bc14fc340582ef148311f924d41815f641a308b5d11e8e7cd" dependencies = [ - "base64-simd 0.8.0", + "base64-simd", "bytes", "bytes-utils", "futures-core", @@ -1408,9 +1323,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.4" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core 0.5.6", "axum-macros", @@ -1429,8 +1344,7 @@ dependencies = [ "multer", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", @@ -1518,7 +1432,7 @@ dependencies = [ "addr2line", "cfg-if", "libc", - "miniz_oxide 0.8.9", + "miniz_oxide", "object", "rustc-demangle", "windows-link 0.2.1", @@ -1554,22 +1468,13 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64-simd" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "781dd20c3aff0bd194fe7d2a977dd92f21c173891f3a03b677359e5fa457e5d5" -dependencies = [ - "simd-abstraction", -] - [[package]] name = "base64-simd" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" dependencies = [ - "outref 0.5.2", + "outref", "vsimd", ] @@ -1581,9 +1486,9 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "better_scoped_tls" -version = "0.1.2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297b153aa5e573b5863108a6ddc9d5c968bd0b20e75cc614ee9821d2f45679c7" +checksum = "7cd228125315b132eed175bf47619ac79b945b26e56b848ba203ae4ea8603609" dependencies = [ "scoped-tls", ] @@ -1610,33 +1515,13 @@ dependencies = [ "serde", ] -[[package]] -name = "bindgen" -version = "0.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" -dependencies = [ - "bitflags 2.9.4", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.117", -] - [[package]] name = "bindgen" version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "cexpr", "clang-sys", "itertools 0.13.0", @@ -1656,7 +1541,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "cexpr", "clang-sys", "itertools 0.13.0", @@ -1670,15 +1555,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec 0.6.3", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -1708,11 +1584,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -1759,19 +1635,13 @@ dependencies = [ "cpufeatures 0.3.0", ] -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "block-padding 0.2.1", + "block-padding", "generic-array", ] @@ -1784,22 +1654,13 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block-buffer" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" -dependencies = [ - "hybrid-array", -] - [[package]] name = "block-modes" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e" dependencies = [ - "block-padding 0.2.1", + "block-padding", "cipher 0.3.0", ] @@ -1809,15 +1670,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" -[[package]] -name = "block-padding" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" -dependencies = [ - "generic-array", -] - [[package]] name = "bollard" version = "0.18.1" @@ -1895,7 +1747,7 @@ checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" dependencies = [ "borsh-derive", "bytes", - "cfg_aliases 0.2.1", + "cfg_aliases", ] [[package]] @@ -1921,17 +1773,6 @@ dependencies = [ "syn 2.0.117", ] -[[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" @@ -1974,6 +1815,15 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bstr" version = "1.12.1" @@ -2072,6 +1922,16 @@ dependencies = [ "serde", ] +[[package]] +name = "bytes-str" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c60b5ce37e0b883c37eb89f79a1e26fbe9c1081945d024eee93e8d91a7e18b3" +dependencies = [ + "bytes", + "serde", +] + [[package]] name = "bytes-utils" version = "0.1.4" @@ -2107,12 +1967,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "cache_control" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf2a5fb3207c12b5d208ebc145f967fea5cac41a021c37417ccc31ba40f39ee" - [[package]] name = "candle-core" version = "0.9.2" @@ -2124,7 +1978,7 @@ dependencies = [ "gemm", "half", "libm", - "memmap2 0.9.10", + "memmap2", "num-traits", "num_cpus", "rand 0.9.0", @@ -2132,7 +1986,7 @@ dependencies = [ "rayon", "safetensors", "thiserror 2.0.18", - "yoke 0.8.2", + "yoke", "zip", ] @@ -2171,15 +2025,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "capacity_builder" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58ec49028cb308564429cd8fac4ef21290067a0afe8f5955330a8d487d0d790c" -dependencies = [ - "itoa", -] - [[package]] name = "capacity_builder" version = "0.5.0" @@ -2187,8 +2032,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f2d24a6dcf0cd402a21b65d35340f3a49ff3475dc5fdac91d22d2733e6641c6" dependencies = [ "capacity_builder_macros", - "ecow", - "hipstr", "itoa", ] @@ -2203,19 +2046,19 @@ dependencies = [ ] [[package]] -name = "cbc" -version = "0.1.2" +name = "castaway" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" dependencies = [ - "cipher 0.4.4", + "rustversion", ] [[package]] name = "cc" -version = "1.2.61" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "jobserver", @@ -2241,7 +2084,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom 7.1.3", + "nom", ] [[package]] @@ -2250,12 +2093,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - [[package]] name = "cfg_aliases" version = "0.2.1" @@ -2322,7 +2159,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common 0.1.7", + "crypto-common", "inout", ] @@ -2334,7 +2171,7 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading 0.8.9", + "libloading", ] [[package]] @@ -2377,15 +2214,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" -[[package]] -name = "clipboard-win" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" -dependencies = [ - "error-code", -] - [[package]] name = "cmake" version = "0.1.58" @@ -2395,49 +2223,6 @@ dependencies = [ "cc", ] -[[package]] -name = "cmov" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" - -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width 0.1.14", -] - -[[package]] -name = "color-print" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aa954171903797d5623e047d9ab69d91b493657917bdfb8c2c80ecaf9cdb6f4" -dependencies = [ - "color-print-proc-macro", -] - -[[package]] -name = "color-print-proc-macro" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692186b5ebe54007e45a59aea47ece9eb4108e141326c304cdc91699a7118a22" -dependencies = [ - "nom 7.1.3", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - [[package]] name = "colorchoice" version = "1.0.5" @@ -2464,6 +2249,19 @@ dependencies = [ "unicode-width 0.2.2", ] +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -2498,12 +2296,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "const-oid" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" - [[package]] name = "const-random" version = "0.1.18" @@ -2624,17 +2416,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core-graphics-types" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "libc", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -2788,25 +2569,16 @@ dependencies = [ "typenum", ] -[[package]] -name = "crypto-common" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" -dependencies = [ - "hybrid-array", -] - [[package]] name = "csv" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" dependencies = [ "csv-core", "itoa", "ryu", - "serde", + "serde_core", ] [[package]] @@ -2827,15 +2599,6 @@ dependencies = [ "cipher 0.4.4", ] -[[package]] -name = "ctutils" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" -dependencies = [ - "cmov", -] - [[package]] name = "curl-sys" version = "0.4.88+curl-8.20.0" @@ -2861,7 +2624,7 @@ dependencies = [ "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest 0.10.7", - "fiat-crypto 0.2.9", + "fiat-crypto", "rustc_version 0.4.1", "subtle", "zeroize", @@ -2878,17 +2641,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "d3d12" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b28bfe653d79bd16c77f659305b195b82bb5ce0c0eb2a4846b82ddbd77586813" -dependencies = [ - "bitflags 2.9.4", - "libloading 0.8.9", - "winapi", -] - [[package]] name = "darling" version = "0.13.4" @@ -2919,16 +2671,6 @@ dependencies = [ "darling_macro 0.20.11", ] -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - [[package]] name = "darling" version = "0.23.0" @@ -2981,20 +2723,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.11.1", - "syn 2.0.117", -] - [[package]] name = "darling_core" version = "0.23.0" @@ -3041,17 +2769,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core 0.21.3", - "quote", - "syn 2.0.117", -] - [[package]] name = "darling_macro" version = "0.23.0" @@ -3063,19 +2780,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "dashmap" version = "6.1.0" @@ -3165,7 +2869,7 @@ checksum = "61fe34f401bd03724a1f96d12108144f8cd495a3cdda2bf5e091822fb80b7e66" dependencies = [ "arrow", "async-trait", - "dashmap 6.1.0", + "dashmap", "datafusion-common", "datafusion-common-runtime", "datafusion-datasource", @@ -3371,7 +3075,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06f004d100f49a3658c9da6fb0c3a9b760062d96cd4ad82ccc3b7b69a9fb2f84" dependencies = [ "arrow", - "dashmap 6.1.0", + "dashmap", "datafusion-common", "datafusion-expr", "futures", @@ -3667,7 +3371,7 @@ checksum = "ad229a134c7406c057ece00c8743c0c34b97f4e72f78b475fe17b66c5e14fa4f" dependencies = [ "arrow", "async-trait", - "dashmap 6.1.0", + "dashmap", "datafusion-common", "datafusion-common-runtime", "datafusion-execution", @@ -3724,19 +3428,18 @@ dependencies = [ [[package]] name = "deno_ast" -version = "0.44.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eebc7aaabfdb3ddcad32aee1b62d250149dc8b35dfbdccbb125df2bdc62da952" +checksum = "c72e0409b3dbd60a5bf296cbc273a8e36bb3a3aab6abac389f1891e187d5ce14" dependencies = [ - "base64 0.21.7", - "deno_error", + "base64 0.22.1", + "capacity_builder", + "deno_error 0.7.3", "deno_media_type", "deno_terminal", "dprint-swc-ext", - "once_cell", "percent-encoding", "serde", - "sourcemap 9.3.2", "swc_atoms", "swc_common", "swc_config", @@ -3744,6 +3447,7 @@ dependencies = [ "swc_ecma_ast", "swc_ecma_codegen", "swc_ecma_codegen_macros", + "swc_ecma_lexer", "swc_ecma_loader", "swc_ecma_parser", "swc_ecma_transforms_base", @@ -3756,146 +3460,45 @@ dependencies = [ "swc_ecma_visit", "swc_eq_ignore_macros", "swc_macros_common", + "swc_sourcemap", "swc_visit", - "swc_visit_macros", "text_lines", "thiserror 2.0.18", - "unicode-width 0.1.14", - "url", -] - -[[package]] -name = "deno_broadcast_channel" -version = "0.184.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33db5dacb54c6fda4c5ea4103c5687b76a51202343379af8b21120ba9d20f3c2" -dependencies = [ - "async-trait", - "deno_core", - "deno_error", - "thiserror 2.0.18", - "tokio", - "uuid", -] - -[[package]] -name = "deno_cache" -version = "0.122.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0daca6ec4e6142a994d38e7bc587dda7948fa00e6194b671a5d4340f5a918a3" -dependencies = [ - "async-trait", - "deno_core", - "deno_error", - "rusqlite", - "serde", - "sha2 0.10.9", - "thiserror 2.0.18", - "tokio", -] - -[[package]] -name = "deno_cache_dir" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27429da4d0e601baaa41415a43468d49a586645d13497f12e8a9346f9f6b1347" -dependencies = [ - "async-trait", - "base32", - "base64 0.21.7", - "boxed_error", - "cache_control", - "chrono", - "data-url", - "deno_error", - "deno_media_type", - "deno_path_util", - "http 1.4.0", - "indexmap 2.14.0", - "log", - "once_cell", - "parking_lot", - "serde", - "serde_json", - "sha2 0.10.9", - "sys_traits", - "thiserror 1.0.69", - "url", -] - -[[package]] -name = "deno_canvas" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ca8f93d60d96d6f6cb0da632303afb98567accf07d9b6f8d2ef88617589d9e" -dependencies = [ - "deno_core", - "deno_error", - "deno_webgpu", - "image", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "deno_config" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08fe512a72c4300bd997c6849450a1f050da0c909a2a4fbdc44891647392bacf" -dependencies = [ - "boxed_error", - "capacity_builder 0.5.0", - "deno_error", - "deno_package_json", - "deno_path_util", - "deno_semver", - "glob", - "ignore", - "import_map", - "indexmap 2.14.0", - "jsonc-parser", - "log", - "percent-encoding", - "phf 0.11.3", - "serde", - "serde_json", - "sys_traits", - "thiserror 2.0.18", + "unicode-width 0.2.2", "url", ] [[package]] name = "deno_console" -version = "0.190.0" +version = "0.209.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94352b8d75c288a26ef748ad0ddae07e181109374a02c547850f96eef76b5389" +checksum = "66c0b8a65dcb7b38c22e5969c6454b3cb0839b7dcfb7a4f0d904de871a4c4416" dependencies = [ "deno_core", ] [[package]] name = "deno_core" -version = "0.336.0" +version = "0.352.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd50476c4325d5fa52bb906804a1e35b127d2a1dcf674e3447b53dcf25525bf" +checksum = "bf78f3f72ac8e09b18a588bc26a1b3c5ab853e7fb489889b2f5000654d34db5d" dependencies = [ "anyhow", "az", "bincode", - "bit-set 0.5.3", - "bit-vec 0.6.3", + "bit-set", + "bit-vec 0.8.0", "bytes", - "capacity_builder 0.1.3", + "capacity_builder", "cooked-waker", "deno_core_icudata", - "deno_error", + "deno_error 0.6.1", "deno_ops", "deno_path_util", "deno_unsync", "futures", "indexmap 2.14.0", "libc", - "memoffset", "parking_lot", "percent-encoding", "pin-project", @@ -3903,7 +3506,7 @@ dependencies = [ "serde_json", "serde_v8", "smallvec", - "sourcemap 8.0.1", + "sourcemap", "static_assertions", "thiserror 2.0.18", "tokio", @@ -3918,69 +3521,13 @@ version = "0.74.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe4dccb6147bb3f3ba0c7a48e993bfeb999d2c2e47a81badee80e2b370c8d695" -[[package]] -name = "deno_cron" -version = "0.70.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8ec283bef14bcf655b209619766bdeab67f2a5e093991cca73f5d502f7bf6e8" -dependencies = [ - "anyhow", - "async-trait", - "chrono", - "deno_core", - "deno_error", - "saffron", - "thiserror 2.0.18", - "tokio", -] - -[[package]] -name = "deno_crypto" -version = "0.204.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f0493142a437e49b46aa8e08d715942076ff48c3cb776f0b015b4224ed0d37a" -dependencies = [ - "aes 0.8.3", - "aes-gcm", - "aes-kw", - "base64 0.21.7", - "cbc", - "const-oid 0.9.6", - "ctr", - "curve25519-dalek", - "deno_core", - "deno_error", - "deno_web", - "ed448-goldilocks", - "elliptic-curve", - "num-traits", - "once_cell", - "p256", - "p384", - "p521", - "rand 0.8.5", - "ring 0.17.14", - "rsa", - "sec1", - "serde", - "serde_bytes", - "sha1", - "sha2 0.10.9", - "signature", - "spki", - "thiserror 2.0.18", - "tokio", - "uuid", - "x25519-dalek", -] - [[package]] name = "deno_error" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c23dbc46d5804814b08b4675838f9884e3a52916987ec5105af36d42f9911b5" +checksum = "612ec3fc481fea759141b0c57810889b0a4fb6fee8f10748677bfe492fd30486" dependencies = [ - "deno_error_macro", + "deno_error_macro 0.6.1", "libc", "serde", "serde_json", @@ -3989,10 +3536,20 @@ dependencies = [ ] [[package]] -name = "deno_error_macro" -version = "0.5.5" +name = "deno_error" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "babccedee31ce7e57c3e6dff2cb3ab8d68c49d0df8222fe0d11d628e65192790" +checksum = "3007d3f1ea92ea503324ae15883aac0c2de2b8cf6fead62203ff6a67161007ab" +dependencies = [ + "deno_error_macro 0.7.3", + "libc", +] + +[[package]] +name = "deno_error_macro" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8380a4224d5d2c3f84da4d764c4326cac62e9a1e3d4960442d29136fc07be863" dependencies = [ "proc-macro2", "quote", @@ -4000,16 +3557,38 @@ dependencies = [ ] [[package]] -name = "deno_fetch" -version = "0.214.0" +name = "deno_error_macro" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df032ca1f7f06a5cc459189b960793f64d415ddc9f59f262e0ad5059865002d" +checksum = "9b565e60a9685cdf312c888665b5f8647ac692a7da7e058a5e2268a466da8eaf" dependencies = [ - "base64 0.21.7", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "deno_features" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487773bb24b92f3b88d1c9ef0d4d15888641a2e2ea4d4cdfd6fda12cae26317c" +dependencies = [ + "deno_core", + "serde", + "serde_json", +] + +[[package]] +name = "deno_fetch" +version = "0.233.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cbe158bec955790105ded69978b1245b97c65092f7bf184add344c0e78efffa" +dependencies = [ + "base64 0.22.1", "bytes", "data-url", "deno_core", - "deno_error", + "deno_error 0.6.1", "deno_fs", "deno_path_util", "deno_permissions", @@ -4033,45 +3612,23 @@ dependencies = [ "tokio-rustls 0.26.4", "tokio-socks", "tokio-util", + "tokio-vsock", "tower 0.5.3", "tower-http", "tower-service", ] -[[package]] -name = "deno_ffi" -version = "0.177.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfbdc4e55c79ec1bc8a3ac72313e6f70d76340222f1e50c5c91e050296f83544" -dependencies = [ - "deno_core", - "deno_error", - "deno_permissions", - "dlopen2 0.6.1", - "dynasmrt", - "libffi", - "libffi-sys", - "log", - "num-bigint", - "serde", - "serde-value", - "serde_json", - "thiserror 2.0.18", - "tokio", - "winapi", -] - [[package]] name = "deno_fs" -version = "0.100.0" +version = "0.119.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c82f79b71403b93b248727a89746026104b3a5ac1d82e79a02af6fa8e8487666" +checksum = "5ca83a95cea7bcdf19dac1888ef6f04aff69d251640a4d085bc6089c06b4b181" dependencies = [ "async-trait", "base32", "boxed_error", "deno_core", - "deno_error", + "deno_error 0.6.1", "deno_io", "deno_path_util", "deno_permissions", @@ -4087,58 +3644,21 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "deno_http" -version = "0.188.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0b7e7a3bcac31ebd4677a96318003a98f0fda4613f6ba6d7f5ba57928727191" -dependencies = [ - "async-compression", - "async-trait", - "base64 0.21.7", - "brotli 6.0.0", - "bytes", - "cache_control", - "deno_core", - "deno_error", - "deno_net", - "deno_websocket", - "flate2", - "http 0.2.12", - "http 1.4.0", - "httparse", - "hyper 0.14.32", - "hyper 1.9.0", - "hyper-util", - "itertools 0.10.5", - "memmem", - "mime", - "once_cell", - "percent-encoding", - "phf 0.11.3", - "pin-project", - "ring 0.17.14", - "scopeguard", - "serde", - "smallvec", - "thiserror 2.0.18", - "tokio", - "tokio-util", -] - [[package]] name = "deno_io" -version = "0.100.0" +version = "0.119.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e72489fe0dcada08047611d1ab92db1baebf7b606ab7c78790f622ecb30e22b" +checksum = "597aac25be261a1bd545d6f47a80439c60e600c076f9847b3718a592883b2a73" dependencies = [ "async-trait", "deno_core", - "deno_error", + "deno_error 0.6.1", + "deno_subprocess_windows", "filetime", "fs3", "libc", "log", + "nix 0.27.1", "once_cell", "os_pipe", "parking_lot", @@ -4150,88 +3670,24 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "deno_kv" -version = "0.98.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0e3930d0195a3350c05eb9a4bc619ec598ea84ad8bdb9b6c3e4bef798e7cf34" -dependencies = [ - "anyhow", - "async-trait", - "base64 0.21.7", - "boxed_error", - "bytes", - "chrono", - "deno_core", - "deno_error", - "deno_fetch", - "deno_path_util", - "deno_permissions", - "deno_tls", - "denokv_proto", - "denokv_remote", - "denokv_sqlite", - "faster-hex", - "http 1.4.0", - "http-body-util", - "log", - "num-bigint", - "prost", - "prost-build", - "rand 0.8.5", - "rusqlite", - "serde", - "thiserror 2.0.18", - "url", -] - -[[package]] -name = "deno_lockfile" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632e835a53ed667d62fdd766c5780fe8361c831d3e3fbf1a760a0b7896657587" -dependencies = [ - "deno_semver", - "serde", - "serde_json", - "thiserror 2.0.18", -] - [[package]] name = "deno_media_type" -version = "0.2.5" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "600222d059ab31ff31182b3e12615df2134a9e01605836b78ad8df91ba39eab3" +checksum = "9fd0af4161f90b092feb363864a64d7c74e0efc13a15905d0d09df73bb72a123" dependencies = [ "data-url", "serde", "url", ] -[[package]] -name = "deno_napi" -version = "0.121.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13f30bf147cc46dba87e3088d037cf99a2845ead1138033d3b346178cb781558" -dependencies = [ - "deno_core", - "deno_error", - "deno_permissions", - "libc", - "libloading 0.7.4", - "log", - "napi_sym", - "thiserror 2.0.18", - "windows-sys 0.59.0", -] - [[package]] name = "deno_native_certs" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86bc737e098a45aa5742d51ce694ac7236a1e69fb0d9df8c862e9b4c9583c5f9" dependencies = [ - "dlopen2 0.7.0", + "dlopen2", "dlopen2_derive", "once_cell", "rustls-native-certs 0.7.3", @@ -4240,12 +3696,13 @@ dependencies = [ [[package]] name = "deno_net" -version = "0.182.0" +version = "0.201.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab869063cbfe428a707511835865d55247886eb175659e538af4d5096c3d4d9d" +checksum = "eb88f1ea2762065d6cc0316372acc22696ab6004943aa029f80546f9faf1b491" dependencies = [ "deno_core", - "deno_error", + "deno_error 0.6.1", + "deno_features", "deno_permissions", "deno_tls", "hickory-proto", @@ -4254,191 +3711,39 @@ dependencies = [ "quinn", "rustls-tokio-stream", "serde", + "sha2 0.10.9", "socket2 0.5.10", "thiserror 2.0.18", "tokio", -] - -[[package]] -name = "deno_node" -version = "0.128.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9638e803a668b0a5793ff94c9b2e82c54a05d9fc510901e9f3093d2d63dbdaab" -dependencies = [ - "aead-gcm-stream", - "aes 0.8.3", - "async-trait", - "base64 0.21.7", - "blake2", - "boxed_error", - "brotli 6.0.0", - "bytes", - "cbc", - "const-oid 0.9.6", - "ctr", - "data-encoding", - "deno_core", - "deno_error", - "deno_fetch", - "deno_fs", - "deno_io", - "deno_net", - "deno_package_json", - "deno_path_util", - "deno_permissions", - "deno_process", - "deno_whoami", - "der", - "digest 0.10.7", - "dsa", - "ecb", - "ecdsa", - "ed25519-dalek", - "elliptic-curve", - "errno", - "faster-hex", - "h2 0.4.14", - "hkdf", - "http 1.4.0", - "http-body-util", - "hyper 1.9.0", - "hyper-util", - "idna", - "indexmap 2.14.0", - "ipnetwork", - "k256", - "lazy-regex", - "libc", - "libz-sys", - "md-5 0.10.6", - "md4", - "memchr", - "node_resolver", - "num-bigint", - "num-bigint-dig", - "num-integer", - "num-traits", - "once_cell", - "p224", - "p256", - "p384", - "path-clean", - "pbkdf2", - "pkcs8", - "rand 0.8.5", - "regex", - "ring 0.17.14", - "ripemd", - "rsa", - "scrypt", - "sec1", - "serde", - "sha1", - "sha2 0.10.9", - "sha3", - "signature", - "simd-json", - "sm3", - "spki", - "stable_deref_trait", - "sys_traits", - "thiserror 2.0.18", - "tokio", - "tokio-eld", - "url", - "webpki-root-certs 0.26.11", - "winapi", - "windows-sys 0.59.0", - "x25519-dalek", - "x509-parser 0.15.1", - "yoke 0.7.5", -] - -[[package]] -name = "deno_npm" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4adceb4c34f10e837d0e3ae76e88dddefb13e83c05c1ef1699fa5519241c9d27" -dependencies = [ - "async-trait", - "capacity_builder 0.5.0", - "deno_error", - "deno_lockfile", - "deno_semver", - "futures", - "log", - "monch", - "serde", - "serde_json", - "thiserror 2.0.18", + "tokio-vsock", "url", + "web-transport-proto", ] [[package]] name = "deno_ops" -version = "0.212.0" +version = "0.228.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d328067139909aa81522a5d90f119368b541fbddd73ab630e4d9f777865f0d" +checksum = "8bf8dbe5abf37d270bb853c5dfe45fbe3b1b6c453877cc11d7fe84e9862a6dbc" dependencies = [ "indexmap 2.14.0", "proc-macro-rules", "proc-macro2", "quote", "stringcase", - "strum 0.25.0", - "strum_macros 0.25.3", + "strum 0.27.2", + "strum_macros 0.27.2", "syn 2.0.117", "thiserror 2.0.18", ] -[[package]] -name = "deno_os" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8371d206f6265c4e0b74116c1a58cc8c464c45da0b43c1e8c19a911e88feb2b" -dependencies = [ - "deno_core", - "deno_error", - "deno_path_util", - "deno_permissions", - "deno_telemetry", - "libc", - "netif", - "ntapi", - "once_cell", - "serde", - "signal-hook", - "signal-hook-registry", - "thiserror 2.0.18", - "tokio", - "winapi", -] - -[[package]] -name = "deno_package_json" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07d26dbfcc01e636aef86f9baff7faf5338398e74d283d8fe01e39068f48049" -dependencies = [ - "boxed_error", - "deno_error", - "deno_path_util", - "deno_semver", - "indexmap 2.14.0", - "serde", - "serde_json", - "sys_traits", - "thiserror 2.0.18", - "url", -] - [[package]] name = "deno_path_util" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c87b8996966ae1b13ee9c20219b1d10fc53905b9570faae6adfa34614fd15224" +checksum = "516f813389095889776b81cc9108ff6f336fd9409b4b12fc0138aea23d2708e1" dependencies = [ - "deno_error", + "deno_error 0.6.1", "percent-encoding", "sys_traits", "thiserror 2.0.18", @@ -4447,183 +3752,54 @@ dependencies = [ [[package]] name = "deno_permissions" -version = "0.49.0" +version = "0.68.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf879dff0b3de4dbcb78d6dda3a55e711369d5b9f479270a82853ef106c4176" +checksum = "baa14d9c3fbba59836ffbaac7f736a2f48d4a5fc209a2103d1e1b898915232a5" dependencies = [ - "capacity_builder 0.5.0", - "deno_core", - "deno_error", + "capacity_builder", + "deno_error 0.6.1", "deno_path_util", "deno_terminal", + "deno_unsync", "fqdn", + "ipnetwork", "libc", "log", - "once_cell", - "percent-encoding", - "serde", - "thiserror 2.0.18", - "which 6.0.3", - "winapi", -] - -[[package]] -name = "deno_process" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "700f8a2c9d369e7035e693f26a671489a73724450cbbc0a1e32f3966ef2f21fb" -dependencies = [ - "deno_core", - "deno_error", - "deno_fs", - "deno_io", - "deno_os", - "deno_path_util", - "deno_permissions", - "libc", - "log", - "memchr", "nix 0.27.1", - "pin-project-lite", - "rand 0.8.5", - "serde", - "simd-json", - "tempfile", - "thiserror 2.0.18", - "tokio", - "which 6.0.3", - "winapi", - "windows-sys 0.59.0", -] - -[[package]] -name = "deno_resolver" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93c4ceec7b6e22344047b8a5577bb8239dc0a99884c25c1fa7d8611f4c3ed28b" -dependencies = [ - "anyhow", - "async-once-cell", - "async-trait", - "base32", - "boxed_error", - "dashmap 5.5.3", - "deno_cache_dir", - "deno_config", - "deno_error", - "deno_media_type", - "deno_npm", - "deno_package_json", - "deno_path_util", - "deno_semver", - "deno_terminal", - "futures", - "log", - "node_resolver", "once_cell", "parking_lot", + "percent-encoding", + "serde", + "serde_json", "sys_traits", + "temp_deno_which", "thiserror 2.0.18", "url", -] - -[[package]] -name = "deno_runtime" -version = "0.198.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26a54d54ca920e5256c1e910c7574787d009a34afd41b20ad250278bb1aea290" -dependencies = [ - "color-print", - "deno_ast", - "deno_broadcast_channel", - "deno_cache", - "deno_canvas", - "deno_console", - "deno_core", - "deno_cron", - "deno_crypto", - "deno_error", - "deno_fetch", - "deno_ffi", - "deno_fs", - "deno_http", - "deno_io", - "deno_kv", - "deno_napi", - "deno_net", - "deno_node", - "deno_os", - "deno_path_util", - "deno_permissions", - "deno_process", - "deno_resolver", - "deno_telemetry", - "deno_terminal", - "deno_tls", - "deno_url", - "deno_web", - "deno_webgpu", - "deno_webidl", - "deno_websocket", - "deno_webstorage", - "dlopen2 0.6.1", - "encoding_rs", - "fastwebsockets", - "http 1.4.0", - "http-body-util", - "hyper 0.14.32", - "hyper 1.9.0", - "hyper-util", - "libc", - "log", - "nix 0.27.1", - "node_resolver", - "notify", - "ntapi", - "once_cell", - "percent-encoding", - "regex", - "rustyline", - "same-file", - "serde", - "sys_traits", - "tempfile", - "thiserror 2.0.18", - "tokio", - "tokio-metrics", - "twox-hash 1.6.3", - "uuid", - "which 6.0.3", "winapi", "windows-sys 0.59.0", ] [[package]] -name = "deno_semver" -version = "0.7.1" +name = "deno_subprocess_windows" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4775271f9b5602482698f76d24ea9ed8ba27af7f587a7e9a876916300c542435" +checksum = "7bc6b10059f0ccb14c6e0319c5275f0407fb2f9ffe405cd555700561999ea4bf" dependencies = [ - "capacity_builder 0.5.0", - "deno_error", - "ecow", - "hipstr", - "monch", - "once_cell", - "serde", - "thiserror 2.0.18", - "url", + "fastrand", + "futures-channel", + "libc", + "windows-sys 0.59.0", ] [[package]] name = "deno_telemetry" -version = "0.12.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d73802ee27361bbb6c0e3c04a799b39f458afbed1972c4aff0867420d1c36fdb" +checksum = "377581966bd34e85ce230f7f4b1dee7e16c6bf7f845b4690648507ce7ba105d9" dependencies = [ "async-trait", "deno_core", - "deno_error", + "deno_error 0.6.1", "deno_tls", "http-body-util", "hyper 1.9.0", @@ -4644,9 +3820,9 @@ dependencies = [ [[package]] name = "deno_terminal" -version = "0.2.3" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3ba8041ae7319b3ca6a64c399df4112badcbbe0868b4517637647614bede4be" +checksum = "23f71c27009e0141dedd315f1dfa3ebb0a6ca4acce7c080fac576ea415a465f6" dependencies = [ "once_cell", "termcolor", @@ -4654,12 +3830,12 @@ dependencies = [ [[package]] name = "deno_tls" -version = "0.177.0" +version = "0.196.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e3ceb2be448150d8214e8fc454c947e0ea94f6ce16556544f05a67ad5a16b8" +checksum = "a7835eb6a8d114703b0293573fbb8885bc2c81e8c35363b9e89391dd3c541fa6" dependencies = [ "deno_core", - "deno_error", + "deno_error 0.6.1", "deno_native_certs", "rustls 0.23.35", "rustls-pemfile 2.2.0", @@ -4684,27 +3860,26 @@ dependencies = [ [[package]] name = "deno_url" -version = "0.190.0" +version = "0.209.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d79e743ad841f7826d46c6944580f5ba665fe9ab4c31a68c4eed8b5a78225da3" +checksum = "8cccbb10fb59f29b04161055a65f7fe83364f9e8317d4d2ce3bd7979faf63a87" dependencies = [ "deno_core", - "deno_error", - "thiserror 2.0.18", + "deno_error 0.6.1", "urlpattern", ] [[package]] name = "deno_web" -version = "0.221.0" +version = "0.240.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8041ba73bb2f238c61b5e4ed341d2fe1f9464a71115a240ab3390480b3c10e12" +checksum = "1d6b8a97cc90b6aaea20fe200cfa7e5b6953dd33ecda0af50d6d360387c33df4" dependencies = [ "async-trait", - "base64-simd 0.8.0", + "base64-simd", "bytes", "deno_core", - "deno_error", + "deno_error 0.6.1", "deno_permissions", "encoding_rs", "flate2", @@ -4715,197 +3890,40 @@ dependencies = [ "uuid", ] -[[package]] -name = "deno_webgpu" -version = "0.157.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4077584c0ccfde0737e576c396bbf1645f25ed0ebf4f44543e0ad13729285cf3" -dependencies = [ - "deno_core", - "deno_error", - "raw-window-handle", - "serde", - "thiserror 2.0.18", - "tokio", - "wgpu-core", - "wgpu-types", -] - [[package]] name = "deno_webidl" -version = "0.190.0" +version = "0.209.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4ff81a990196bf3a80fe5d339b4eb8b411ef17634d60d399a63bae6e71a37c9" +checksum = "a20bbfac0cf15918f7cbd0b55c366ac20ad65c825f362fa684c3104984254d3b" dependencies = [ "deno_core", ] -[[package]] -name = "deno_websocket" -version = "0.195.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ad15c3856dd1748f9a36102e90b4e345e40d7d64a9bf6672d584201e1fded28" -dependencies = [ - "bytes", - "deno_core", - "deno_error", - "deno_net", - "deno_permissions", - "deno_tls", - "fastwebsockets", - "h2 0.4.14", - "http 1.4.0", - "http-body-util", - "hyper 1.9.0", - "hyper-util", - "once_cell", - "rustls-tokio-stream", - "serde", - "thiserror 2.0.18", - "tokio", -] - -[[package]] -name = "deno_webstorage" -version = "0.185.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "079dc4f6ce91f53bb848bad8d743dc20d16ca44cbed3425531cf5d922b1a45bc" -dependencies = [ - "deno_core", - "deno_error", - "deno_web", - "rusqlite", - "thiserror 2.0.18", -] - -[[package]] -name = "deno_whoami" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e75e4caa92b98a27f09c671d1399aee0f5970aa491b9a598523aac000a2192e3" -dependencies = [ - "libc", - "whoami", -] - -[[package]] -name = "denokv_proto" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5b77de4d3b9215e14624d4f4eb16cb38c0810e3f5860ba3b3fc47d0537f9a4d" -dependencies = [ - "async-trait", - "chrono", - "deno_error", - "futures", - "num-bigint", - "prost", - "serde", - "uuid", -] - -[[package]] -name = "denokv_remote" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6497c28eec268ed99f1e8664f0842935f02d1508529c67d94c57ca5d893d743" -dependencies = [ - "async-stream", - "async-trait", - "bytes", - "chrono", - "deno_error", - "denokv_proto", - "futures", - "http 1.4.0", - "log", - "prost", - "rand 0.8.5", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tokio-util", - "url", - "uuid", -] - -[[package]] -name = "denokv_sqlite" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0f21a450a35eb85760761401fddf9bfff9840127be07a6ca5c31863127913d" -dependencies = [ - "async-stream", - "async-trait", - "chrono", - "deno_error", - "denokv_proto", - "futures", - "hex", - "log", - "num-bigint", - "rand 0.8.5", - "rusqlite", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "uuid", - "v8_valueserializer", -] - [[package]] name = "der" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid 0.9.6", - "der_derive", + "const-oid", "pem-rfc7468", "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 7.1.3", - "num-bigint", - "num-traits", - "rusticata-macros", -] - [[package]] name = "der-parser" version = "9.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" dependencies = [ - "asn1-rs 0.6.2", + "asn1-rs", "displaydoc", - "nom 7.1.3", + "nom", "num-bigint", "num-traits", "rusticata-macros", ] -[[package]] -name = "der_derive" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "deranged" version = "0.5.8" @@ -5047,23 +4065,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid 0.9.6", - "crypto-common 0.1.7", + "const-oid", + "crypto-common", "subtle", ] -[[package]] -name = "digest" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" -dependencies = [ - "block-buffer 0.12.0", - "const-oid 0.10.2", - "crypto-common 0.2.1", - "ctutils", -] - [[package]] name = "dirs" version = "4.0.0" @@ -5158,18 +4164,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "dlopen2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bc2c7ed06fd72a8513ded8d0d2f6fd2655a85d6885c48cae8625d80faf28c03" -dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi", -] - [[package]] name = "dlopen2" version = "0.7.0" @@ -5193,15 +4187,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "document-features" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] - [[package]] name = "dotenv" version = "0.15.0" @@ -5222,35 +4207,20 @@ checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" [[package]] name = "dprint-swc-ext" -version = "0.20.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ba28c12892aadb751c2ba7001d8460faee4748a04b4edc51c7121cc67ee03db" +checksum = "cf592ae6a864437e98ef9c6ae7936b822077e9d038a3a48ee081ab92313afad4" dependencies = [ "num-bigint", - "rustc-hash 1.1.0", + "rustc-hash 2.1.2", "swc_atoms", "swc_common", "swc_ecma_ast", + "swc_ecma_lexer", "swc_ecma_parser", "text_lines", ] -[[package]] -name = "dsa" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48bc224a9084ad760195584ce5abb3c2c34a225fa312a128ad245a6b412b7689" -dependencies = [ - "digest 0.10.7", - "num-bigint-dig", - "num-traits", - "pkcs8", - "rfc6979", - "sha2 0.10.9", - "signature", - "zeroize", -] - [[package]] name = "duct" version = "0.13.7" @@ -5291,41 +4261,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d926b4d407d372f141f93bb444696142c29d32962ccbd3531117cf3aa0bfa9" -[[package]] -name = "dynasm" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add9a102807b524ec050363f09e06f1504214b0e1c7797f64261c891022dce8b" -dependencies = [ - "bitflags 1.3.2", - "byteorder", - "lazy_static", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "dynasmrt" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64fba5a42bd76a17cad4bfa00de168ee1cbfa06a5e8ce992ae880218c05641a9" -dependencies = [ - "byteorder", - "dynasm", - "memmap2 0.5.10", -] - -[[package]] -name = "ecb" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a8bfa975b1aec2145850fcaa1c6fe269a16578c44705a532ae3edc92b8881c7" -dependencies = [ - "cipher 0.4.4", -] - [[package]] name = "ecdsa" version = "0.16.9" @@ -5340,15 +4275,6 @@ dependencies = [ "spki", ] -[[package]] -name = "ecow" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78e4f79b296fbaab6ce2e22d52cb4c7f010fe0ebe7a32e34fa25885fd797bd02" -dependencies = [ - "serde", -] - [[package]] name = "ed25519" version = "2.2.3" @@ -5375,18 +4301,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ed448-goldilocks" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06924531e9e90130842b012e447f85bdaf9161bc8a0f8092be8cb70b01ebe092" -dependencies = [ - "fiat-crypto 0.1.20", - "hex", - "subtle", - "zeroize", -] - [[package]] name = "educe" version = "0.6.0" @@ -5415,7 +4329,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", - "base64ct", "crypto-bigint", "digest 0.10.7", "ff", @@ -5426,8 +4339,6 @@ dependencies = [ "pkcs8", "rand_core 0.6.4", "sec1", - "serde_json", - "serdect", "subtle", "zeroize", ] @@ -5440,19 +4351,13 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] -[[package]] -name = "endian-type" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" - [[package]] name = "enum-as-inner" version = "0.6.1" @@ -5532,12 +4437,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "error-code" -version = "3.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" - [[package]] name = "error_reporter" version = "1.0.0" @@ -5598,7 +4497,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" dependencies = [ "futures-core", - "nom 7.1.3", + "nom", "pin-project-lite", ] @@ -5608,25 +4507,13 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - [[package]] name = "fancy-regex" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" dependencies = [ - "bit-set 0.8.0", + "bit-set", "regex-automata", "regex-syntax 0.8.10", ] @@ -5637,7 +4524,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8" dependencies = [ - "bit-set 0.8.0", + "bit-set", "regex-automata", "regex-syntax 0.8.10", ] @@ -5648,61 +4535,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afc2bd4d5a73106dd53d10d73d3401c2f32730ba2c0b93ddb888a8983680471" -[[package]] -name = "faster-hex" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183" -dependencies = [ - "serde", -] - [[package]] name = "fastrand" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" -[[package]] -name = "fastwebsockets" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dac026e15fb7e44d768880b868a0fd5bd30ffdee272e88b3060f657a5a72947" -dependencies = [ - "base64 0.21.7", - "bytes", - "http-body-util", - "hyper 1.9.0", - "hyper-util", - "pin-project", - "rand 0.8.5", - "sha1", - "simdutf8", - "thiserror 1.0.69", - "tokio", - "utf-8", -] - -[[package]] -name = "fd-lock" -version = "4.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" -dependencies = [ - "cfg-if", - "rustix 1.1.4", - "windows-sys 0.59.0", -] - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - [[package]] name = "ff" version = "0.13.1" @@ -5713,12 +4551,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "fiat-crypto" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" - [[package]] name = "fiat-crypto" version = "0.2.9" @@ -5727,13 +4559,12 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" -version = "0.2.27" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +checksum = "2d5b2eef6fafbf69f877e55509ce5b11a760690ac9700a2921be067aa6afaef6" dependencies = [ "cfg-if", "libc", - "libredox", ] [[package]] @@ -5754,7 +4585,7 @@ version = "25.12.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "rustc_version 0.4.1", ] @@ -5766,19 +4597,10 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "libz-sys", - "miniz_oxide 0.8.9", + "miniz_oxide", "zlib-rs", ] -[[package]] -name = "float-cmp" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" -dependencies = [ - "num-traits", -] - [[package]] name = "float8" version = "0.6.1" @@ -5827,28 +4649,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared 0.1.1", -] - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared 0.3.1", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "foreign-types-shared", ] [[package]] @@ -5857,12 +4658,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -5880,11 +4675,10 @@ checksum = "eb540cf7bc4fe6df9d8f7f0c974cfd0dce8ed4e9e8884e73433b503ee78b4e7d" [[package]] name = "from_variant" -version = "0.1.9" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32016f1242eb82af5474752d00fd8ebcd9004bd69b462b1c91de833972d08ed4" +checksum = "308530a56b099da144ebc5d8e179f343ad928fa2b3558d1eb3db9af18d6eff43" dependencies = [ - "proc-macro2", "swc_macros_common", "syn 2.0.117", ] @@ -5916,15 +4710,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" -[[package]] -name = "fsevent-sys" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] - [[package]] name = "fslock" version = "0.2.1" @@ -6330,17 +5115,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "gl_generator" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" -dependencies = [ - "khronos_api", - "log", - "xml-rs", -] - [[package]] name = "glob" version = "0.3.3" @@ -6372,27 +5146,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "glow" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" -dependencies = [ - "js-sys", - "slotmap", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "glutin_wgl_sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8098adac955faa2d31079b65dc48841251f69efd3ac25477903fc424362ead" -dependencies = [ - "gl_generator", -] - [[package]] name = "google-cloud-auth" version = "0.17.2" @@ -6493,45 +5246,6 @@ dependencies = [ "unic-ucd-category", ] -[[package]] -name = "gpu-alloc" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" -dependencies = [ - "bitflags 2.9.4", - "gpu-alloc-types", -] - -[[package]] -name = "gpu-alloc-types" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" -dependencies = [ - "bitflags 2.9.4", -] - -[[package]] -name = "gpu-descriptor" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" -dependencies = [ - "bitflags 2.9.4", - "gpu-descriptor-types", - "hashbrown 0.15.5", -] - -[[package]] -name = "gpu-descriptor-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" -dependencies = [ - "bitflags 2.9.4", -] - [[package]] name = "group" version = "0.13.0" @@ -6605,16 +5319,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "halfbrown" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8588661a8607108a5ca69cab034063441a0413a0b041c13618a7dd348021ef6f" -dependencies = [ - "hashbrown 0.14.5", - "serde", -] - [[package]] name = "hashbrown" version = "0.12.3" @@ -6647,21 +5351,22 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", "foldhash 0.2.0", "serde", + "serde_core", ] [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hashify" @@ -6675,15 +5380,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "hashlink" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" -dependencies = [ - "hashbrown 0.14.5", -] - [[package]] name = "hashlink" version = "0.10.0" @@ -6693,20 +5389,6 @@ dependencies = [ "hashbrown 0.15.5", ] -[[package]] -name = "hdrhistogram" -version = "7.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" -dependencies = [ - "base64 0.21.7", - "byteorder", - "crossbeam-channel", - "flate2", - "nom 7.1.3", - "num-traits", -] - [[package]] name = "headers" version = "0.4.1" @@ -6755,12 +5437,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hexf-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" - [[package]] name = "hf-hub" version = "0.4.3" @@ -6833,24 +5509,13 @@ dependencies = [ "tracing", ] -[[package]] -name = "hipstr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97971ffc85d4c98de12e2608e992a43f5294ebb625fdb045b27c731b64c4c6d6" -dependencies = [ - "serde", - "serde_bytes", - "sptr", -] - [[package]] name = "hkdf" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac 0.12.1", + "hmac", ] [[package]] @@ -6862,15 +5527,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "hmac" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" -dependencies = [ - "digest 0.11.3", -] - [[package]] name = "home" version = "0.5.12" @@ -6893,15 +5549,14 @@ dependencies = [ [[package]] name = "hstr" -version = "0.2.17" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a26def229ea95a8709dad32868d975d0dd40235bd2ce82920e4a8fe692b5e0" +checksum = "31f11d91d7befd2ffd9d216e9e5ea1fae6174b20a2a1b67a688138003d2f4122" dependencies = [ "hashbrown 0.14.5", "new_debug_unreachable", "once_cell", - "phf 0.11.3", - "rustc-hash 1.1.0", + "rustc-hash 2.1.2", "triomphe", ] @@ -7013,15 +5668,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" -[[package]] -name = "hybrid-array" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" -dependencies = [ - "typenum", -] - [[package]] name = "hyper" version = "0.14.32" @@ -7276,7 +5922,7 @@ dependencies = [ "displaydoc", "potential_utf", "utf8_iter", - "yoke 0.8.2", + "yoke", "zerofrom", "zerovec", ] @@ -7343,7 +5989,7 @@ dependencies = [ "displaydoc", "icu_locale_core", "writeable", - "yoke 0.8.2", + "yoke", "zerofrom", "zerotrie", "zerovec", @@ -7388,52 +6034,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb" -[[package]] -name = "ignore" -version = "0.4.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" -dependencies = [ - "crossbeam-deque", - "globset", - "log", - "memchr", - "regex-automata", - "same-file", - "walkdir", - "winapi-util", -] - -[[package]] -name = "image" -version = "0.24.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" -dependencies = [ - "bytemuck", - "byteorder", - "color_quant", - "num-traits", - "png", -] - -[[package]] -name = "import_map" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1215d4d92511fbbdaea50e750e91f2429598ef817f02b579158e92803b52c00a" -dependencies = [ - "boxed_error", - "deno_error", - "indexmap 2.14.0", - "log", - "percent-encoding", - "serde", - "serde_json", - "thiserror 2.0.18", - "url", -] - [[package]] name = "indexmap" version = "1.9.3" @@ -7452,7 +6052,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -7470,33 +6070,12 @@ dependencies = [ "web-time", ] -[[package]] -name = "inotify" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" -dependencies = [ - "bitflags 1.3.2", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] - [[package]] name = "inout" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "block-padding 0.3.3", "generic-array", ] @@ -7521,7 +6100,7 @@ version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "cfg-if", "libc", ] @@ -7696,15 +6275,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "jsonc-parser" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6d80e6d70e7911a29f3cf3f44f452df85d06f73572b494ca99a2cad3fcf8f4" -dependencies = [ - "serde_json", -] - [[package]] name = "jsonpath-rust" version = "0.7.5" @@ -7767,20 +6337,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "k256" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" -dependencies = [ - "cfg-if", - "ecdsa", - "elliptic-curve", - "once_cell", - "sha2 0.10.9", - "signature", -] - [[package]] name = "k8s-openapi" version = "0.25.0" @@ -7793,15 +6349,6 @@ 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 = "keyed_priority_queue" version = "0.4.2" @@ -7811,23 +6358,6 @@ dependencies = [ "indexmap 2.14.0", ] -[[package]] -name = "khronos-egl" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" -dependencies = [ - "libc", - "libloading 0.8.9", - "pkg-config", -] - -[[package]] -name = "khronos_api" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" - [[package]] name = "konst" version = "0.2.20" @@ -7843,26 +6373,6 @@ version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" -[[package]] -name = "kqueue" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - [[package]] name = "kube" version = "1.1.0" @@ -7979,29 +6489,6 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" -[[package]] -name = "lazy-regex" -version = "3.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bae91019476d3ec7147de9aa291cadb6d870abf2f3015d2da73a90325ac1496" -dependencies = [ - "lazy-regex-proc_macros", - "once_cell", - "regex", -] - -[[package]] -name = "lazy-regex-proc_macros" -version = "3.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de9c1e1439d8b7b3061b2d209809f447ca33241733d9a3c01eabf2dc8d94358" -dependencies = [ - "proc-macro2", - "quote", - "regex", - "syn 2.0.117", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -8086,16 +6573,6 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" -[[package]] -name = "libffi" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce826c243048e3d5cec441799724de52e2d42f820468431fc3fceee2341871e2" -dependencies = [ - "libc", - "libffi-sys", -] - [[package]] name = "libffi-sys" version = "2.3.0" @@ -8111,7 +6588,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e668df13f2e97f3eed52d9301f6b1c4c1ccfccc30eab9e6628e4a8c1fc3546" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "bytes", "lazy_static", "libgssapi-sys", @@ -8127,16 +6604,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - [[package]] name = "libloading" version = "0.8.9" @@ -8170,7 +6637,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "libc", "plain", "redox_syscall 0.7.5", @@ -8182,7 +6649,6 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ - "cc", "pkg-config", "vcpkg", ] @@ -8244,12 +6710,6 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - [[package]] name = "lock_api" version = "0.4.14" @@ -8296,7 +6756,7 @@ version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" dependencies = [ - "hashbrown 0.16.0", + "hashbrown 0.16.1", ] [[package]] @@ -8320,14 +6780,14 @@ version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" dependencies = [ - "twox-hash 2.1.2", + "twox-hash", ] [[package]] name = "lz4_flex" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db9a0d582c2874f68138a16ce1867e0ffde6c0bb0a0df85e1f36d04146db488a" +checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" [[package]] name = "lzma-sys" @@ -8477,15 +6937,6 @@ dependencies = [ "malachite-nz", ] -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - [[package]] name = "mappable-rc" version = "0.1.1" @@ -8540,25 +6991,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "md-5" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" -dependencies = [ - "cfg-if", - "digest 0.11.3", -] - -[[package]] -name = "md4" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da5ac363534dce5fabf69949225e174fbf111a498bf0ff794c8ea1fba9f3dda" -dependencies = [ - "digest 0.10.7", -] - [[package]] name = "md5" version = "0.6.1" @@ -8580,15 +7012,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "memmap2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" -dependencies = [ - "libc", -] - [[package]] name = "memmap2" version = "0.9.10" @@ -8599,12 +7022,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "memmem" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" - [[package]] name = "memoffset" version = "0.9.1" @@ -8614,21 +7031,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "metal" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5637e166ea14be6063a3f8ba5ccb9a4159df7d8f6d61c02fc3d480b1f90dcfcb" -dependencies = [ - "bitflags 2.9.4", - "block", - "core-graphics-types", - "foreign-types 0.5.0", - "log", - "objc", - "paste", -] - [[package]] name = "miette" version = "7.6.0" @@ -8689,15 +7091,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -dependencies = [ - "adler", -] - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -8708,18 +7101,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.48.0", -] - [[package]] name = "mio" version = "1.2.0" @@ -8751,12 +7132,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "monch" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b52c1b33ff98142aecea13138bd399b68aa7ab5d9546c300988c345004001eea" - [[package]] name = "monostate" version = "0.1.18" @@ -8796,12 +7171,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "multimap" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" - [[package]] name = "murmurhash32" version = "0.3.1" @@ -8852,7 +7221,7 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-util", - "twox-hash 2.1.2", + "twox-hash", "url", ] @@ -8863,7 +7232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbb9f371618ce723f095c61fbcdc36e8936956d2b62832f9c7648689b338e052" dependencies = [ "base64 0.22.1", - "bitflags 2.9.4", + "bitflags 2.11.1", "btoi", "byteorder", "bytes", @@ -8884,28 +7253,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "naga" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e536ae46fcab0876853bd4a632ede5df4b1c2527a58f6c5a4150fe86be858231" -dependencies = [ - "arrayvec", - "bit-set 0.5.3", - "bitflags 2.9.4", - "codespan-reporting", - "hexf-parse", - "indexmap 2.14.0", - "log", - "num-traits", - "rustc-hash 1.1.0", - "serde", - "spirv", - "termcolor", - "thiserror 1.0.69", - "unicode-xid", -] - [[package]] name = "nanorand" version = "0.7.0" @@ -8915,18 +7262,6 @@ dependencies = [ "getrandom 0.2.17", ] -[[package]] -name = "napi_sym" -version = "0.120.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33a55ec137cebb7f4a594edd16157a5b9d9addf7ebd29c88198ec4e0cff2e93e" -dependencies = [ - "quote", - "serde", - "serde_json", - "syn 2.0.117", -] - [[package]] name = "native-tls" version = "0.2.16" @@ -8939,52 +7274,24 @@ dependencies = [ "openssl-probe 0.2.1", "openssl-sys", "schannel", - "security-framework 3.6.0", + "security-framework 3.7.0", "security-framework-sys", "tempfile", ] -[[package]] -name = "ndk-sys" -version = "0.5.0+25.2.9519653" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" -dependencies = [ - "jni-sys 0.3.1", -] - -[[package]] -name = "netif" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29a01b9f018d6b7b277fef6c79fdbd9bf17bb2d1e298238055cafab49baa5ee" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "new_debug_unreachable" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nibble_vec" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" -dependencies = [ - "smallvec", -] - [[package]] name = "nix" version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "cfg-if", "libc", ] @@ -8995,9 +7302,9 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "cfg-if", - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", ] @@ -9007,12 +7314,25 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "cfg-if", - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", ] +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "nkeys" version = "0.4.5" @@ -9028,42 +7348,6 @@ dependencies = [ "signatory", ] -[[package]] -name = "node_resolver" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808426e80ce77a311b24ac080caf18c23c632e035d797edb217ce74cdf6a0e71" -dependencies = [ - "anyhow", - "async-trait", - "boxed_error", - "dashmap 5.5.3", - "deno_error", - "deno_media_type", - "deno_package_json", - "deno_path_util", - "futures", - "lazy-regex", - "once_cell", - "path-clean", - "regex", - "serde", - "serde_json", - "sys_traits", - "thiserror 2.0.18", - "url", -] - -[[package]] -name = "nom" -version = "5.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" -dependencies = [ - "memchr", - "version_check", -] - [[package]] name = "nom" version = "7.1.3" @@ -9074,25 +7358,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "notify" -version = "6.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" -dependencies = [ - "bitflags 2.9.4", - "crossbeam-channel", - "filetime", - "fsevent-sys", - "inotify", - "kqueue", - "libc", - "log", - "mio 0.8.11", - "walkdir", - "windows-sys 0.48.0", -] - [[package]] name = "ntapi" version = "0.4.3" @@ -9288,7 +7553,6 @@ dependencies = [ "num-iter", "num-traits", "rand 0.8.5", - "serde", "smallvec", "zeroize", ] @@ -9418,15 +7682,6 @@ dependencies = [ "url", ] -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - [[package]] name = "object" version = "0.37.3" @@ -9482,22 +7737,13 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" dependencies = [ - "asn1-rs 0.6.2", + "asn1-rs", ] [[package]] @@ -9530,7 +7776,7 @@ version = "6.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "libc", "once_cell", "onig_sys", @@ -9562,7 +7808,7 @@ dependencies = [ "chrono", "dyn-clone", "ed25519-dalek", - "hmac 0.12.1", + "hmac", "http 1.4.0", "itertools 0.10.5", "log", @@ -9589,9 +7835,9 @@ version = "0.10.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "cfg-if", - "foreign-types 0.3.2", + "foreign-types", "libc", "openssl-macros", "openssl-sys", @@ -9804,6 +8050,8 @@ dependencies = [ "rand 0.8.5", "serde_json", "thiserror 1.0.69", + "tokio", + "tokio-stream", "tracing", ] @@ -9878,20 +8126,14 @@ dependencies = [ [[package]] name = "os_pipe" -version = "1.1.5" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9" +checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] -[[package]] -name = "outref" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4" - [[package]] name = "outref" version = "0.5.2" @@ -9912,18 +8154,6 @@ version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" -[[package]] -name = "p224" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30c06436d66652bc2f01ade021592c80a2aad401570a18aa18b82e440d2b9aa1" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2 0.10.9", -] - [[package]] name = "p256" version = "0.13.2" @@ -9949,17 +8179,12 @@ dependencies = [ ] [[package]] -name = "p521" -version = "0.13.3" +name = "par-core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +checksum = "e96cbd21255b7fb29a5d51ef38a779b517a91abd59e2756c039583f43ef4c90f" dependencies = [ - "base16ct", - "ecdsa", - "elliptic-curve", - "primeorder", - "rand_core 0.6.4", - "sha2 0.10.9", + "once_cell", ] [[package]] @@ -10023,7 +8248,7 @@ dependencies = [ "snap", "thrift", "tokio", - "twox-hash 2.1.2", + "twox-hash", "zstd", ] @@ -10050,28 +8275,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5a797f0e07bdf071d15742978fc3128ec6c22891c31a3a931513263904c982a" -[[package]] -name = "path-clean" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecba01bf2678719532c5e3059e0b5f0811273d94b397088b82e3bd0a78c78fdd" - [[package]] name = "pathdiff" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest 0.10.7", - "hmac 0.12.1", -] - [[package]] name = "pem" version = "1.1.1" @@ -10199,6 +8408,16 @@ dependencies = [ "phf_shared 0.12.1", ] +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared 0.13.1", + "serde", +] + [[package]] name = "phf_codegen" version = "0.11.3" @@ -10250,6 +8469,15 @@ dependencies = [ "siphasher 1.0.3", ] +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.3", +] + [[package]] name = "php-parser-rs" version = "0.1.3" @@ -10305,21 +8533,6 @@ dependencies = [ "spki", ] -[[package]] -name = "pkcs5" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" -dependencies = [ - "aes 0.8.3", - "cbc", - "der", - "pbkdf2", - "scrypt", - "sha2 0.10.9", - "spki", -] - [[package]] name = "pkcs8" version = "0.10.2" @@ -10327,8 +8540,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der", - "pkcs5", - "rand_core 0.6.4", "spki", ] @@ -10344,19 +8555,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide 0.8.9", -] - [[package]] name = "polyval" version = "0.6.2" @@ -10377,85 +8575,56 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "postgres-native-tls" -version = "0.5.0" -source = "git+https://github.com/imor/rust-postgres?rev=20265ef38e32a06f76b6f9b678e2077fc2211f6b#20265ef38e32a06f76b6f9b678e2077fc2211f6b" +version = "0.5.2" +source = "git+https://github.com/MaterializeInc/rust-postgres?rev=78c1222577bb091d69bc22b1bc7ad01c14675abe#78c1222577bb091d69bc22b1bc7ad01c14675abe" dependencies = [ "native-tls", "tokio", "tokio-native-tls", - "tokio-postgres 0.7.11", + "tokio-postgres", ] [[package]] name = "postgres-native-tls" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f39498473c92f7b6820ae970382c1d83178a3454c618161cb772e8598d9f6f" +checksum = "fef4de47bb81477e0c3deaf153a1b10ae176484713ff1640969f4cb96b653ebc" dependencies = [ "native-tls", "tokio", "tokio-native-tls", - "tokio-postgres 0.7.13", + "tokio-postgres", ] [[package]] name = "postgres-protocol" -version = "0.6.7" -source = "git+https://github.com/imor/rust-postgres?rev=20265ef38e32a06f76b6f9b678e2077fc2211f6b#20265ef38e32a06f76b6f9b678e2077fc2211f6b" +version = "0.6.9" +source = "git+https://github.com/MaterializeInc/rust-postgres?rev=78c1222577bb091d69bc22b1bc7ad01c14675abe#78c1222577bb091d69bc22b1bc7ad01c14675abe" dependencies = [ "base64 0.22.1", "byteorder", "bytes", - "fallible-iterator 0.2.0", - "hmac 0.12.1", + "fallible-iterator", + "hmac", "md-5 0.10.6", "memchr", - "rand 0.8.5", + "rand 0.9.0", "sha2 0.10.9", "stringprep", ] -[[package]] -name = "postgres-protocol" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56201207dac53e2f38e848e31b4b91616a6bb6e0c7205b77718994a7f49e70fc" -dependencies = [ - "base64 0.22.1", - "byteorder", - "bytes", - "fallible-iterator 0.2.0", - "hmac 0.13.0", - "md-5 0.11.0", - "memchr", - "rand 0.10.1", - "sha2 0.11.0", - "stringprep", -] - [[package]] name = "postgres-types" -version = "0.2.7" -source = "git+https://github.com/imor/rust-postgres?rev=20265ef38e32a06f76b6f9b678e2077fc2211f6b#20265ef38e32a06f76b6f9b678e2077fc2211f6b" -dependencies = [ - "bytes", - "fallible-iterator 0.2.0", - "postgres-protocol 0.6.7", -] - -[[package]] -name = "postgres-types" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613283563cd90e1dfc3518d548caee47e0e725455ed619881f5cf21f36de4b48" +version = "0.2.11" +source = "git+https://github.com/MaterializeInc/rust-postgres?rev=78c1222577bb091d69bc22b1bc7ad01c14675abe#78c1222577bb091d69bc22b1bc7ad01c14675abe" dependencies = [ "array-init", "bit-vec 0.6.3", "bytes", "chrono", - "fallible-iterator 0.2.0", - "postgres-protocol 0.6.11", - "serde", + "fallible-iterator", + "postgres-protocol", + "serde_core", "serde_json", "uuid", ] @@ -10511,11 +8680,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.23.4", + "toml_edit 0.25.11+spec-1.1.0", ] [[package]] @@ -10527,7 +8696,6 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn 1.0.109", "version_check", ] @@ -10616,7 +8784,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "chrono", "flate2", "hex", @@ -10630,17 +8798,11 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "chrono", "hex", ] -[[package]] -name = "profiling" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" - [[package]] name = "prometheus" version = "0.14.0" @@ -10665,26 +8827,6 @@ dependencies = [ "prost-derive", ] -[[package]] -name = "prost-build" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" -dependencies = [ - "heck 0.5.0", - "itertools 0.14.0", - "log", - "multimap", - "once_cell", - "petgraph", - "prettyplease", - "prost", - "prost-types", - "regex", - "syn 2.0.117", - "tempfile", -] - [[package]] name = "prost-derive" version = "0.13.5" @@ -10743,7 +8885,7 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "getopts", "memchr", "unicase", @@ -10806,7 +8948,7 @@ checksum = "5a70b1b8b47e31d0498ecbc3c5470bb931399a8bfed1fd79d1717a61ce7f96e3" dependencies = [ "ahash 0.8.12", "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "parking_lot", ] @@ -10817,7 +8959,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", - "cfg_aliases 0.2.1", + "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", @@ -10858,7 +9000,7 @@ version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", "once_cell", "socket2 0.6.3", @@ -10893,16 +9035,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" -[[package]] -name = "radix_trie" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" -dependencies = [ - "endian-type", - "nibble_vec", -] - [[package]] name = "rand" version = "0.7.3" @@ -11031,27 +9163,15 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "range-alloc" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" - [[package]] name = "raw-cpuid" version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", ] -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - [[package]] name = "rayon" version = "1.12.0" @@ -11093,7 +9213,7 @@ dependencies = [ "ring 0.17.14", "rustls-pki-types", "time", - "x509-parser 0.16.0", + "x509-parser", "yasna", ] @@ -11163,7 +9283,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", ] [[package]] @@ -11172,7 +9292,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", ] [[package]] @@ -11414,11 +9534,11 @@ checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" [[package]] name = "retry-policies" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503c78f59814e2664c9980b739b19e40f549233ecf39928040ee54fdd431f614" +checksum = "dc05fbf560421a0357a750cbe78c7ca19d4923918490daabba313d5dbc871e47" dependencies = [ - "rand 0.8.5", + "rand 0.10.1", ] [[package]] @@ -11427,7 +9547,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac 0.12.1", + "hmac", "subtle", ] @@ -11460,15 +9580,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "ripemd" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" -dependencies = [ - "digest 0.10.7", -] - [[package]] name = "rkyv" version = "0.7.46" @@ -11505,7 +9616,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bef41ebc9ebed2c1b1d90203e9d1756091e8a00bbc3107676151f39868ca0ee" dependencies = [ "async-trait", - "axum 0.8.4", + "axum 0.8.9", "base64 0.22.1", "bytes", "chrono", @@ -11546,18 +9657,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "ron" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" -dependencies = [ - "base64 0.21.7", - "bitflags 2.9.4", - "serde", - "serde_derive", -] - [[package]] name = "rquickjs" version = "0.11.0" @@ -11575,7 +9674,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bf7840285c321c3ab20e752a9afb95548c75cd7f4632a0627cea3507e310c1" dependencies = [ "async-lock", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "relative-path", "rquickjs-sys", ] @@ -11612,7 +9711,7 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid 0.9.6", + "const-oid", "digest 0.10.7", "num-bigint-dig", "num-integer", @@ -11646,20 +9745,6 @@ dependencies = [ "tokio-rustls 0.25.0", ] -[[package]] -name = "rusqlite" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" -dependencies = [ - "bitflags 2.9.4", - "fallible-iterator 0.3.0", - "fallible-streaming-iterator", - "hashlink 0.9.1", - "libsqlite3-sys", - "smallvec", -] - [[package]] name = "rust-embed" version = "6.8.1" @@ -11715,7 +9800,7 @@ dependencies = [ "borsh", "bytes", "num-traits", - "postgres-types 0.2.9", + "postgres-types", "rand 0.8.5", "rkyv", "serde", @@ -11765,7 +9850,7 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" dependencies = [ - "nom 7.1.3", + "nom", ] [[package]] @@ -11774,7 +9859,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -11787,7 +9872,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.12.1", @@ -11870,7 +9955,7 @@ dependencies = [ "openssl-probe 0.2.1", "rustls-pki-types", "schannel", - "security-framework 3.6.0", + "security-framework 3.7.0", ] [[package]] @@ -11916,9 +10001,9 @@ dependencies = [ "rustls-native-certs 0.8.3", "rustls-platform-verifier-android", "rustls-webpki 0.103.13", - "security-framework 3.6.0", + "security-framework 3.7.0", "security-framework-sys", - "webpki-root-certs 1.0.7", + "webpki-root-certs", "windows-sys 0.61.2", ] @@ -11930,9 +10015,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-tokio-stream" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22557157d7395bc30727745b365d923f1ecc230c4c80b176545f3f4f08c46e33" +checksum = "faa7dc7c991d9164e55bbf1558029eb5b84d32cc4d61a7df5b8641b2deedc4b3" dependencies = [ "futures", "rustls 0.23.35", @@ -12036,28 +10121,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "rustyline" -version = "13.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02a2d683a4ac90aeef5b1013933f6d977bd37d51ff3f4dad829d4931a7e6be86" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "clipboard-win", - "fd-lock", - "home", - "libc", - "log", - "memchr", - "nix 0.27.1", - "radix_trie", - "unicode-segmentation", - "unicode-width 0.1.14", - "utf8parse", - "winapi", -] - [[package]] name = "ryu" version = "1.0.23" @@ -12076,30 +10139,11 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "675656c1eabb620b921efea4f9199f97fc86e36dd6ffd1fbbe48d0f59a4987f5" dependencies = [ - "hashbrown 0.16.0", + "hashbrown 0.16.1", "serde", "serde_json", ] -[[package]] -name = "saffron" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03fb9a628596fc7590eb7edbf7b0613287be78df107f5f97b118aad59fb2eea9" -dependencies = [ - "chrono", - "nom 5.1.3", -] - -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher 0.4.4", -] - [[package]] name = "samael" version = "0.0.20" @@ -12245,18 +10289,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scrypt" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" -dependencies = [ - "password-hash", - "pbkdf2", - "salsa20", - "sha2 0.10.9", -] - [[package]] name = "sct" version = "0.7.1" @@ -12289,7 +10321,6 @@ dependencies = [ "der", "generic-array", "pkcs8", - "serdect", "subtle", "zeroize", ] @@ -12309,7 +10340,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -12318,11 +10349,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -12368,9 +10399,9 @@ checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" [[package]] name = "serde" -version = "1.0.220" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceecad4c782e936ac90ecfd6b56532322e3262b14320abf30ce89a92ffdbfe22" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -12409,30 +10440,20 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "serde_bytes" -version = "0.11.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" -dependencies = [ - "serde", - "serde_core", -] - [[package]] name = "serde_core" -version = "1.0.220" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddba47394f3b862d6ff6efdbd26ca4673e3566a307880a0ffb98f274bbe0ec32" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.220" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60e1f3b1761e96def5ec6d04a6e7421c0404fa3cf5c0155f1e2848fae3d8cc08" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -12527,11 +10548,11 @@ dependencies = [ [[package]] name = "serde_v8" -version = "0.245.0" +version = "0.261.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945f93c91e0c7e4799b5fefff076756141aae92e262c4dc4833310dd3d2d845e" +checksum = "3495190857461e87a2716141043218aad5281f219f54a03b7ebbe605b3b931df" dependencies = [ - "deno_error", + "deno_error 0.6.1", "num-bigint", "serde", "smallvec", @@ -12541,19 +10562,19 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" dependencies = [ "base64 0.22.1", + "bs58", "chrono", "hex", "indexmap 1.9.3", "indexmap 2.14.0", "schemars 0.9.0", "schemars 1.2.1", - "serde", - "serde_derive", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -12561,11 +10582,11 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" dependencies = [ - "darling 0.21.3", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.117", @@ -12599,16 +10620,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "serdect" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" -dependencies = [ - "base16ct", - "serde", -] - [[package]] name = "serial_test" version = "3.4.0" @@ -12670,27 +10681,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "sha2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "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", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -12725,16 +10715,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[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" @@ -12767,36 +10747,12 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "simd-abstraction" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cadb29c57caadc51ff8346233b5cec1d240b68ce55cf1afc764818791876987" -dependencies = [ - "outref 0.1.0", -] - [[package]] name = "simd-adler32" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" -[[package]] -name = "simd-json" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2bcf6c6e164e81bc7a5d49fc6988b3d515d9e8c07457d7b74ffb9324b9cd40" -dependencies = [ - "getrandom 0.2.17", - "halfbrown", - "ref-cast", - "serde", - "serde_json", - "simdutf8", - "value-trait", -] - [[package]] name = "simdutf8" version = "0.1.5" @@ -12848,24 +10804,6 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" -[[package]] -name = "slotmap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" -dependencies = [ - "version_check", -] - -[[package]] -name = "sm3" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebb9a3b702d0a7e33bc4d85a14456633d2b165c2ad839c5fd9a8417c1ab15860" -dependencies = [ - "digest 0.10.7", -] - [[package]] name = "smallvec" version = "1.15.1" @@ -12929,32 +10867,13 @@ dependencies = [ "winapi", ] -[[package]] -name = "sourcemap" -version = "8.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "208d40b9e8cad9f93613778ea295ed8f3c2b1824217c6cfc7219d3f6f45b96d4" -dependencies = [ - "base64-simd 0.7.0", - "bitvec", - "data-encoding", - "debugid", - "if_chain", - "rustc-hash 1.1.0", - "rustc_version 0.2.3", - "serde", - "serde_json", - "unicode-id-start", - "url", -] - [[package]] name = "sourcemap" version = "9.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "314d62a489431668f719ada776ca1d49b924db951b7450f8974c9ae51ab05ad7" dependencies = [ - "base64-simd 0.8.0", + "base64-simd", "bitvec", "data-encoding", "debugid", @@ -12981,15 +10900,6 @@ dependencies = [ "lock_api", ] -[[package]] -name = "spirv" -version = "0.3.0+sdk-1.3.268.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" -dependencies = [ - "bitflags 2.9.4", -] - [[package]] name = "spki" version = "0.7.3" @@ -13007,17 +10917,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326" dependencies = [ "base64 0.13.1", - "nom 7.1.3", + "nom", "serde", "unicode-segmentation", ] -[[package]] -name = "sptr" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" - [[package]] name = "sql-builder" version = "3.1.1" @@ -13093,7 +10997,7 @@ dependencies = [ "futures-io", "futures-util", "hashbrown 0.15.5", - "hashlink 0.10.0", + "hashlink", "indexmap 2.14.0", "log", "memchr", @@ -13160,7 +11064,7 @@ dependencies = [ "atoi", "base64 0.22.1", "bigdecimal", - "bitflags 2.9.4", + "bitflags 2.11.1", "byteorder", "bytes", "chrono", @@ -13175,7 +11079,7 @@ dependencies = [ "generic-array", "hex", "hkdf", - "hmac 0.12.1", + "hmac", "itoa", "log", "md-5 0.10.6", @@ -13205,7 +11109,7 @@ dependencies = [ "atoi", "base64 0.22.1", "bigdecimal", - "bitflags 2.9.4", + "bitflags 2.11.1", "byteorder", "chrono", "crc", @@ -13216,7 +11120,7 @@ dependencies = [ "futures-util", "hex", "hkdf", - "hmac 0.12.1", + "hmac", "home", "itoa", "log", @@ -13303,11 +11207,10 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "string_enum" -version = "0.4.4" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05e383308aebc257e7d7920224fa055c632478d92744eca77f99be8fa1545b90" +checksum = "ae36a4951ca7bd1cfd991c241584a9824a70f6aff1e7d4f693fb3f2465e4030e" dependencies = [ - "proc-macro2", "quote", "swc_macros_common", "syn 2.0.117", @@ -13315,9 +11218,9 @@ dependencies = [ [[package]] name = "stringcase" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04028eeb851ed08af6aba5caa29f2d59a13ed168cee4d6bd753aeefcf1d636b0" +checksum = "72abeda133c49d7bddece6c154728f83eec8172380c80ab7096da9487e20d27c" [[package]] name = "stringprep" @@ -13423,64 +11326,48 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" [[package]] name = "swc_allocator" -version = "0.1.10" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76aa0eb65c0f39f9b6d82a7e5192c30f7ac9a78f084a21f270de1d8c600ca388" +checksum = "9d7eefd2c8b228a8c73056482b2ae4b3a1071fbe07638e3b55ceca8570cc48bb" dependencies = [ + "allocator-api2", "bumpalo", "hashbrown 0.14.5", - "ptr_meta", - "rustc-hash 1.1.0", - "triomphe", + "rustc-hash 2.1.2", ] [[package]] name = "swc_atoms" -version = "0.6.7" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb6567e4e67485b3e7662b486f1565bdae54bd5b9d6b16b2ba1a9babb1e42125" +checksum = "3500dcf04c84606b38464561edc5e46f5132201cb3e23cf9613ed4033d6b1bb2" dependencies = [ "hstr", "once_cell", - "rustc-hash 1.1.0", - "serde", -] - -[[package]] -name = "swc_cached" -version = "0.3.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83406221c501860fce9c27444f44125eafe9e598b8b81be7563d7036784cd05c" -dependencies = [ - "ahash 0.8.12", - "anyhow", - "dashmap 5.5.3", - "once_cell", - "regex", "serde", ] [[package]] name = "swc_common" -version = "0.37.5" +version = "14.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12d0a8eaaf1606c9207077d75828008cb2dfb51b095a766bd2b72ef893576e31" +checksum = "c2bb772b3a26b8b71d4e8c112ced5b5867be2266364b58517407a270328a2696" dependencies = [ + "anyhow", "ast_node", "better_scoped_tls", - "cfg-if", + "bytes-str", "either", "from_variant", "new_debug_unreachable", "num-bigint", "once_cell", - "rustc-hash 1.1.0", + "rustc-hash 2.1.2", "serde", "siphasher 0.3.11", - "sourcemap 9.3.2", - "swc_allocator", "swc_atoms", "swc_eq_ignore_macros", + "swc_sourcemap", "swc_visit", "tracing", "unicode-width 0.1.14", @@ -13489,23 +11376,23 @@ dependencies = [ [[package]] name = "swc_config" -version = "0.1.15" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4740e53eaf68b101203c1df0937d5161a29f3c13bceed0836ddfe245b72dd000" +checksum = "72e90b52ee734ded867104612218101722ad87ff4cf74fe30383bd244a533f97" dependencies = [ "anyhow", + "bytes-str", "indexmap 2.14.0", "serde", "serde_json", - "swc_cached", "swc_config_macro", ] [[package]] name = "swc_config_macro" -version = "0.1.4" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c5f56139042c1a95b54f5ca48baa0e0172d369bcc9d3d473dad1de36bae8399" +checksum = "7b416e8ce6de17dc5ea496e10c7012b35bbc0e3fef38d2e065eed936490db0b3" dependencies = [ "proc-macro2", "quote", @@ -13515,78 +11402,72 @@ dependencies = [ [[package]] name = "swc_ecma_ast" -version = "0.118.2" +version = "15.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f866d12e4d519052b92a0a86d1ac7ff17570da1272ca0c89b3d6f802cd79df" +checksum = "65c25af97d53cf8aab66a6c68f3418663313fc969ad267fc2a4d19402c329be1" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "is-macro", "num-bigint", + "once_cell", "phf 0.11.3", - "scoped-tls", + "rustc-hash 2.1.2", "serde", "string_enum", "swc_atoms", "swc_common", + "swc_visit", "unicode-id-start", ] [[package]] name = "swc_ecma_codegen" -version = "0.155.1" +version = "17.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7641608ef117cfbef9581a99d02059b522fcca75e5244fa0cbbd8606689c6f" +checksum = "bcf55c2d7555c93f4945e29f93b7529562be97ba16e60dd94c25724d746174ac" dependencies = [ + "ascii", + "compact_str", "memchr", "num-bigint", "once_cell", + "regex", + "rustc-hash 2.1.2", + "ryu-js", "serde", - "sourcemap 9.3.2", "swc_allocator", "swc_atoms", "swc_common", "swc_ecma_ast", "swc_ecma_codegen_macros", + "swc_sourcemap", "tracing", ] [[package]] name = "swc_ecma_codegen_macros" -version = "0.7.7" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859fabde36db38634f3fad548dd5e3410c1aebba1b67a3c63e67018fa57a0bca" +checksum = "e276dc62c0a2625a560397827989c82a93fd545fcf6f7faec0935a82cc4ddbb8" dependencies = [ "proc-macro2", - "quote", "swc_macros_common", "syn 2.0.117", ] [[package]] -name = "swc_ecma_loader" -version = "0.49.1" +name = "swc_ecma_lexer" +version = "23.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55fa3d55045b97894bfb04d38aff6d6302ac8a6a38e3bb3dfb0d20475c4974a9" -dependencies = [ - "anyhow", - "pathdiff", - "serde", - "swc_atoms", - "swc_common", - "tracing", -] - -[[package]] -name = "swc_ecma_parser" -version = "0.149.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683dada14722714588b56481399c699378b35b2ba4deb5c4db2fb627a97fb54b" +checksum = "017d06ea85008234aa9fb34d805c7dc563f2ea6e03869ed5ac5a2dc27d561e4d" dependencies = [ + "arrayvec", + "bitflags 2.11.1", "either", - "new_debug_unreachable", "num-bigint", - "num-traits", "phf 0.11.3", + "rustc-hash 2.1.2", + "seq-macro", "serde", "smallvec", "smartstring", @@ -13595,23 +11476,52 @@ dependencies = [ "swc_common", "swc_ecma_ast", "tracing", - "typed-arena", +] + +[[package]] +name = "swc_ecma_loader" +version = "14.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c675d14700c92f12585049b22b02356f1e142f4b0c32a4d0eb4b7a968a4c0c1e" +dependencies = [ + "anyhow", + "pathdiff", + "rustc-hash 2.1.2", + "serde", + "swc_atoms", + "swc_common", + "tracing", +] + +[[package]] +name = "swc_ecma_parser" +version = "24.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9011783c975ba592ffc09cd208ced92b1dfabb2e5e0ef453559e2e25286127" +dependencies = [ + "either", + "num-bigint", + "serde", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_lexer", + "tracing", ] [[package]] name = "swc_ecma_transforms_base" -version = "0.145.0" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65f21494e75d0bd8ef42010b47cabab9caaed8f2207570e809f6f4eb51a710d1" +checksum = "6c6f1b8f4232e7a7f614ff7c0f6ccb89c2d028cdf7629f79ad710cff5b28b62c" dependencies = [ "better_scoped_tls", - "bitflags 2.9.4", "indexmap 2.14.0", "once_cell", + "par-core", "phf 0.11.3", - "rustc-hash 1.1.0", + "rustc-hash 2.1.2", "serde", - "smallvec", "swc_atoms", "swc_common", "swc_ecma_ast", @@ -13623,11 +11533,10 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_classes" -version = "0.134.0" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3d884594385bea9405a2e1721151470d9a14d3ceec5dd773c0ca6894791601" +checksum = "108d4d52db6151f768a516fe86e6f21fc783b03fa2d20292999f29275fd0c71d" dependencies = [ - "swc_atoms", "swc_common", "swc_ecma_ast", "swc_ecma_transforms_base", @@ -13637,9 +11546,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_macros" -version = "0.5.5" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500a1dadad1e0e41e417d633b3d6d5de677c9e0d3159b94ba3348436cdb15aab" +checksum = "bc777288799bf6786e5200325a56e4fbabba590264a4a48a0c70b16ad0cf5cd8" dependencies = [ "proc-macro2", "quote", @@ -13649,56 +11558,54 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_proposal" -version = "0.179.0" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79938ff510fc647febd8c6c3ef4143d099fdad87a223680e632623d056dae2dd" +checksum = "39b3b34f6a28348416174912009d09994ab71c867682ec78d641a9feb3a96b4e" dependencies = [ "either", - "rustc-hash 1.1.0", + "rustc-hash 2.1.2", "serde", - "smallvec", "swc_atoms", "swc_common", "swc_ecma_ast", "swc_ecma_transforms_base", "swc_ecma_transforms_classes", - "swc_ecma_transforms_macros", "swc_ecma_utils", "swc_ecma_visit", ] [[package]] name = "swc_ecma_transforms_react" -version = "0.191.0" +version = "30.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76c76d8b9792ce51401d38da0fa62158d61f6d80d16d68fe5b03ce4bf5fba383" +checksum = "69ea0052ac23b5b9fbc85bbdb1791b36b918f9d55f594b0ed8e25babb4c32d16" dependencies = [ - "base64 0.21.7", - "dashmap 5.5.3", + "base64 0.22.1", + "bytes-str", "indexmap 2.14.0", "once_cell", + "rustc-hash 2.1.2", "serde", "sha1", "string_enum", - "swc_allocator", "swc_atoms", "swc_common", "swc_config", "swc_ecma_ast", "swc_ecma_parser", "swc_ecma_transforms_base", - "swc_ecma_transforms_macros", "swc_ecma_utils", "swc_ecma_visit", ] [[package]] name = "swc_ecma_transforms_typescript" -version = "0.198.1" +version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15455da4768f97186c40523e83600495210c11825d3a44db43383fd81eace88d" +checksum = "3872c006ccfdcc19f1cf5c01c15915a69964ba7982c9f581cdb7e727e77b9a2c" dependencies = [ - "ryu-js", + "bytes-str", + "rustc-hash 2.1.2", "serde", "swc_atoms", "swc_common", @@ -13711,28 +11618,28 @@ dependencies = [ [[package]] name = "swc_ecma_utils" -version = "0.134.2" +version = "21.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029eec7dd485923a75b5a45befd04510288870250270292fc2c1b3a9e7547408" +checksum = "83259addd99ed4022aa9fc4d39428c008d3d42533769e1a005529da18cde4568" dependencies = [ "indexmap 2.14.0", "num_cpus", "once_cell", - "rustc-hash 1.1.0", + "par-core", + "rustc-hash 2.1.2", "ryu-js", "swc_atoms", "swc_common", "swc_ecma_ast", "swc_ecma_visit", "tracing", - "unicode-id", ] [[package]] name = "swc_ecma_visit" -version = "0.104.8" +version = "15.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1c6802e68e51f336e8bc9644e9ff9da75d7da9c1a6247d532f2e908aa33e81" +checksum = "75a579aa8f9e212af521588df720ccead079c09fe5c8f61007cf724324aed3a0" dependencies = [ "new_debug_unreachable", "num-bigint", @@ -13745,9 +11652,9 @@ dependencies = [ [[package]] name = "swc_eq_ignore_macros" -version = "0.1.4" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63db0adcff29d220c3d151c5b25c0eabe7e32dd936212b84cdaa1392e3130497" +checksum = "c16ce73424a6316e95e09065ba6a207eba7765496fed113702278b7711d4b632" dependencies = [ "proc-macro2", "quote", @@ -13756,38 +11663,44 @@ dependencies = [ [[package]] name = "swc_macros_common" -version = "0.3.13" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f486687bfb7b5c560868f69ed2d458b880cebc9babebcb67e49f31b55c5bf847" +checksum = "aae1efbaa74943dc5ad2a2fb16cbd78b77d7e4d63188f3c5b4df2b4dcd2faaae" dependencies = [ "proc-macro2", "quote", "syn 2.0.117", ] +[[package]] +name = "swc_sourcemap" +version = "9.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de08ef00f816acdd1a58ee8a81c0e1a59eefef2093aefe5611f256fa6b64c4d7" +dependencies = [ + "base64-simd", + "bitvec", + "bytes-str", + "data-encoding", + "debugid", + "if_chain", + "rustc-hash 2.1.2", + "serde", + "serde_json", + "unicode-id-start", + "url", +] + [[package]] name = "swc_visit" -version = "0.6.2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ceb044142ba2719ef9eb3b6b454fce61ab849eb696c34d190f04651955c613d" +checksum = "62fb71484b486c185e34d2172f0eabe7f4722742aad700f426a494bb2de232a2" dependencies = [ "either", "new_debug_unreachable", ] -[[package]] -name = "swc_visit_macros" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92807d840959f39c60ce8a774a3f83e8193c658068e6d270dbe0a05e40e90b41" -dependencies = [ - "Inflector", - "proc-macro2", - "quote", - "swc_macros_common", - "syn 2.0.117", -] - [[package]] name = "symlink" version = "0.1.0" @@ -13825,18 +11738,6 @@ 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" @@ -13859,12 +11760,22 @@ dependencies = [ [[package]] name = "sys_traits" -version = "0.1.7" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b46ac05dfbe9fd3a9703eff20e17f5b31e7b6a54daf27a421dcd56c7a27ecdd" +checksum = "dc4707edf3196e8037ee45018d1bb1bfb233b0e4fc440fa3d3f25bc69bfdaf26" dependencies = [ - "libc", - "windows-sys 0.59.0", + "sys_traits_macros", +] + +[[package]] +name = "sys_traits_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "181f22127402abcf8ee5c83ccd5b408933fec36a6095cf82cda545634692657e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -13873,7 +11784,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "byteorder", "enum-as-inner", "libc", @@ -13901,7 +11812,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -13925,7 +11836,7 @@ dependencies = [ "bytesize", "lazy_static", "libc", - "nom 7.1.3", + "nom", "time", "winapi", ] @@ -13960,9 +11871,9 @@ dependencies = [ "levenshtein_automata", "log", "lru 0.16.4", - "lz4_flex 0.13.0", + "lz4_flex 0.13.1", "measure_time", - "memmap2 0.9.10", + "memmap2", "once_cell", "oneshot", "rayon", @@ -14040,7 +11951,7 @@ version = "0.25.0" source = "git+https://github.com/windmill-labs/tantivy?rev=6ae7c70bc603b8e69e27f3240e08bd00a93fb12c#6ae7c70bc603b8e69e27f3240e08bd00a93fb12c" dependencies = [ "fnv", - "nom 7.1.3", + "nom", "ordered-float 5.3.0", "serde", "serde_json", @@ -14093,6 +12004,15 @@ dependencies = [ "xattr", ] +[[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" @@ -14399,7 +12319,7 @@ dependencies = [ "bytes", "io-uring", "libc", - "mio 1.2.0", + "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -14410,16 +12330,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "tokio-eld" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9166030f05d6bc5642bdb8f8c2be31eb3c02cd465d662bcdc2df82d4aa41a584" -dependencies = [ - "hdrhistogram", - "tokio", -] - [[package]] name = "tokio-graceful" version = "0.1.6" @@ -14444,18 +12354,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "tokio-metrics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eace09241d62c98b7eeb1107d4c5c64ca3bd7da92e8c218c153ab3a78f9be112" -dependencies = [ - "futures-util", - "pin-project-lite", - "tokio", - "tokio-stream", -] - [[package]] name = "tokio-native-tls" version = "0.3.1" @@ -14468,50 +12366,24 @@ dependencies = [ [[package]] name = "tokio-postgres" -version = "0.7.11" -source = "git+https://github.com/imor/rust-postgres?rev=20265ef38e32a06f76b6f9b678e2077fc2211f6b#20265ef38e32a06f76b6f9b678e2077fc2211f6b" +version = "0.7.15" +source = "git+https://github.com/MaterializeInc/rust-postgres?rev=78c1222577bb091d69bc22b1bc7ad01c14675abe#78c1222577bb091d69bc22b1bc7ad01c14675abe" dependencies = [ "async-trait", "byteorder", "bytes", - "fallible-iterator 0.2.0", + "fallible-iterator", "futures-channel", "futures-util", "log", "parking_lot", "percent-encoding", - "phf 0.11.3", + "phf 0.13.1", "pin-project-lite", - "postgres-protocol 0.6.7", - "postgres-types 0.2.7", - "rand 0.8.5", - "socket2 0.5.10", - "tokio", - "tokio-util", - "whoami", -] - -[[package]] -name = "tokio-postgres" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c95d533c83082bb6490e0189acaa0bbeef9084e60471b696ca6988cd0541fb0" -dependencies = [ - "async-trait", - "byteorder", - "bytes", - "fallible-iterator 0.2.0", - "futures-channel", - "futures-util", - "log", - "parking_lot", - "percent-encoding", - "phf 0.11.3", - "pin-project-lite", - "postgres-protocol 0.6.11", - "postgres-types 0.2.9", + "postgres-protocol", + "postgres-types", "rand 0.9.0", - "socket2 0.5.10", + "socket2 0.6.3", "tokio", "tokio-util", "whoami", @@ -14624,12 +12496,24 @@ dependencies = [ "futures-io", "futures-sink", "futures-util", - "hashbrown 0.15.5", "pin-project-lite", "slab", "tokio", ] +[[package]] +name = "tokio-vsock" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b319ef9394889dab2e1b4f0085b45ba11d0c79dc9d1a9d1afc057d009d0f1c7" +dependencies = [ + "bytes", + "futures", + "libc", + "tokio", + "vsock", +] + [[package]] name = "tokio-websockets" version = "0.10.1" @@ -14674,11 +12558,11 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -14696,14 +12580,14 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.4" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7211ff1b8f0d3adae1663b7da9ffe396eabe1ca25f0b0bee42b0da29a9ddce93" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ "indexmap 2.14.0", - "toml_datetime 0.7.0", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 0.7.15", + "winnow 1.0.2", ] [[package]] @@ -14756,7 +12640,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" dependencies = [ "async-trait", - "axum 0.8.4", + "axum 0.8.9", "base64 0.22.1", "bytes", "h2 0.4.14", @@ -14843,7 +12727,7 @@ checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" dependencies = [ "async-compression", "base64 0.22.1", - "bitflags 2.9.4", + "bitflags 2.11.1", "bytes", "futures-core", "futures-util", @@ -15114,29 +12998,12 @@ dependencies = [ "utf-8", ] -[[package]] -name = "twox-hash" -version = "1.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" -dependencies = [ - "cfg-if", - "rand 0.8.5", - "static_assertions", -] - [[package]] name = "twox-hash" version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" -[[package]] -name = "typed-arena" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" - [[package]] name = "typed-path" version = "0.12.3" @@ -15272,12 +13139,6 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" -[[package]] -name = "unicode-id" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ba288e709927c043cbe476718d37be306be53fb1fafecd0dbe36d072be2580" - [[package]] name = "unicode-id-start" version = "1.4.0" @@ -15378,7 +13239,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common 0.1.7", + "crypto-common", "subtle", ] @@ -15489,66 +13350,38 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.2", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] [[package]] name = "v8" -version = "130.0.7" +version = "137.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a511192602f7b435b0a241c1947aa743eb7717f20a9195f4b5e8ed1952e01db1" +checksum = "33995a1fee055ff743281cde33a41f0d618ee0bdbe8bdf6859e11864499c2595" dependencies = [ - "bindgen 0.70.1", - "bitflags 2.9.4", + "bindgen 0.71.1", + "bitflags 2.11.1", "fslock", "gzip-header", "home", - "miniz_oxide 0.7.4", - "once_cell", + "miniz_oxide", "paste", "which 6.0.3", ] -[[package]] -name = "v8_valueserializer" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97599c400fc79925922b58303e98fcb8fa88f573379a08ddb652e72cbd2e70f6" -dependencies = [ - "bitflags 2.9.4", - "encoding_rs", - "indexmap 2.14.0", - "num-bigint", - "serde", - "thiserror 1.0.69", - "wtf8", -] - [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "value-trait" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9170e001f458781e92711d2ad666110f153e4e50bfd5cbd02db6547625714187" -dependencies = [ - "float-cmp", - "halfbrown", - "itoa", - "ryu", -] - [[package]] name = "vcpkg" version = "0.2.15" @@ -15567,6 +13400,16 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" +[[package]] +name = "vsock" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba782755fc073877e567c2253c0be48e4aa9a254c232d36d3985dfae0bd5205" +dependencies = [ + "libc", + "nix 0.31.3", +] + [[package]] name = "vte" version = "0.14.1" @@ -15765,11 +13608,11 @@ dependencies = [ [[package]] name = "wasm_dep_analyzer" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eeee3bdea6257cc36d756fa745a70f9d393571e47d69e0ed97581676a5369ca" +checksum = "e51cf5f08b357e64cd7642ab4bbeb11aecab9e15520692129624fb9908b8df2c" dependencies = [ - "deno_error", + "deno_error 0.6.1", "thiserror 2.0.18", ] @@ -15779,7 +13622,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap 2.14.0", "semver 1.0.28", @@ -15820,12 +13663,15 @@ dependencies = [ ] [[package]] -name = "webpki-root-certs" -version = "0.26.11" +name = "web-transport-proto" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +checksum = "974fa1e325e6cc5327de8887f189a441fcff4f8eedcd31ec87f0ef0cc5283fbc" dependencies = [ - "webpki-root-certs 1.0.7", + "bytes", + "http 1.4.0", + "thiserror 2.0.18", + "url", ] [[package]] @@ -15855,89 +13701,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "wgpu-core" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50819ab545b867d8a454d1d756b90cd5f15da1f2943334ca314af10583c9d39" -dependencies = [ - "arrayvec", - "bit-vec 0.6.3", - "bitflags 2.9.4", - "cfg_aliases 0.1.1", - "codespan-reporting", - "document-features", - "indexmap 2.14.0", - "log", - "naga", - "once_cell", - "parking_lot", - "profiling", - "raw-window-handle", - "ron", - "rustc-hash 1.1.0", - "serde", - "smallvec", - "thiserror 1.0.69", - "web-sys", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-hal" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "172e490a87295564f3fcc0f165798d87386f6231b04d4548bca458cbbfd63222" -dependencies = [ - "android_system_properties", - "arrayvec", - "ash", - "bit-set 0.5.3", - "bitflags 2.9.4", - "block", - "cfg_aliases 0.1.1", - "core-graphics-types", - "d3d12", - "glow", - "glutin_wgl_sys", - "gpu-alloc", - "gpu-descriptor", - "js-sys", - "khronos-egl", - "libc", - "libloading 0.8.9", - "log", - "metal", - "naga", - "ndk-sys", - "objc", - "once_cell", - "parking_lot", - "profiling", - "range-alloc", - "raw-window-handle", - "rustc-hash 1.1.0", - "smallvec", - "thiserror 1.0.69", - "wasm-bindgen", - "web-sys", - "wgpu-types", - "winapi", -] - -[[package]] -name = "wgpu-types" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1353d9a46bff7f955a680577f34c69122628cc2076e1d6f3a9be6ef00ae793ef" -dependencies = [ - "bitflags 2.9.4", - "js-sys", - "serde", - "web-sys", -] - [[package]] name = "which" version = "4.4.2" @@ -16025,14 +13788,14 @@ dependencies = [ [[package]] name = "windmill" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-nats", "aws-config", "aws-credential-types", "aws-sdk-sqs", - "axum 0.8.4", + "axum 0.8.9", "base64 0.22.1", "chrono", "constant_time_eq 0.3.1", @@ -16106,7 +13869,7 @@ dependencies = [ [[package]] name = "windmill-ai" -version = "1.697.0" +version = "1.700.2" dependencies = [ "async-trait", "aws-config", @@ -16114,13 +13877,19 @@ dependencies = [ "aws-sdk-bedrockruntime", "aws-smithy-types", "base64 0.22.1", + "bytes", + "eventsource-stream", + "futures", "lazy_static", + "mime_guess", "reqwest 0.13.1", "serde", "serde_json", "sqlx", "tokio", + "tokio-stream", "tracing", + "ulid", "uuid", "windmill-common", "windmill-mcp", @@ -16130,9 +13899,9 @@ dependencies = [ [[package]] name = "windmill-alerting" -version = "1.697.0" +version = "1.700.2" dependencies = [ - "axum 0.8.4", + "axum 0.8.9", "chrono", "serde", "serde_json", @@ -16143,7 +13912,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "argon2", @@ -16159,7 +13928,7 @@ dependencies = [ "aws-sdk-config", "aws-sigv4", "aws-smithy-types", - "axum 0.8.4", + "axum 0.8.9", "base32", "base64 0.22.1", "bytes", @@ -16168,7 +13937,7 @@ dependencies = [ "const_format", "cookie", "cron", - "dashmap 6.1.0", + "dashmap", "datafusion", "ed25519-dalek", "eventsource-stream", @@ -16176,7 +13945,7 @@ dependencies = [ "futures", "git-version", "hex", - "hmac 0.12.1", + "hmac", "http 1.4.0", "hyper 1.9.0", "indexmap 2.14.0", @@ -16192,7 +13961,7 @@ dependencies = [ "openidconnect", "openssl", "pin-project", - "postgres-native-tls 0.5.1", + "postgres-native-tls 0.5.3", "prometheus", "quick_cache", "rand 0.9.0", @@ -16215,7 +13984,7 @@ dependencies = [ "time", "tokio", "tokio-native-tls", - "tokio-postgres 0.7.13", + "tokio-postgres", "tokio-stream", "tokio-util", "tower 0.5.3", @@ -16286,9 +14055,9 @@ dependencies = [ [[package]] name = "windmill-api-agent-workers" -version = "1.697.0" +version = "1.700.2" dependencies = [ - "axum 0.8.4", + "axum 0.8.9", "chrono", "http 1.4.0", "hyper 1.9.0", @@ -16309,9 +14078,9 @@ dependencies = [ [[package]] name = "windmill-api-assets" -version = "1.697.0" +version = "1.700.2" dependencies = [ - "axum 0.8.4", + "axum 0.8.9", "chrono", "serde", "serde_json", @@ -16322,10 +14091,10 @@ dependencies = [ [[package]] name = "windmill-api-auth" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", - "axum 0.8.4", + "axum 0.8.9", "chrono", "http 1.4.0", "itertools 0.14.0", @@ -16348,7 +14117,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.697.0" +version = "1.700.2" dependencies = [ "reqwest 0.12.28", "serde", @@ -16358,9 +14127,9 @@ dependencies = [ [[package]] name = "windmill-api-configs" -version = "1.697.0" +version = "1.700.2" dependencies = [ - "axum 0.8.4", + "axum 0.8.9", "chrono", "itertools 0.14.0", "serde", @@ -16375,9 +14144,9 @@ dependencies = [ [[package]] name = "windmill-api-debug" -version = "1.697.0" +version = "1.700.2" dependencies = [ - "axum 0.8.4", + "axum 0.8.9", "base64 0.22.1", "chrono", "ed25519-dalek", @@ -16397,10 +14166,10 @@ dependencies = [ [[package]] name = "windmill-api-embeddings" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", - "axum 0.8.4", + "axum 0.8.9", "candle-core", "candle-nn", "candle-transformers", @@ -16420,9 +14189,9 @@ dependencies = [ [[package]] name = "windmill-api-flow-conversations" -version = "1.697.0" +version = "1.700.2" dependencies = [ - "axum 0.8.4", + "axum 0.8.9", "chrono", "serde", "sql-builder", @@ -16436,9 +14205,9 @@ dependencies = [ [[package]] name = "windmill-api-flows" -version = "1.697.0" +version = "1.700.2" dependencies = [ - "axum 0.8.4", + "axum 0.8.9", "chrono", "hyper 1.9.0", "serde", @@ -16457,9 +14226,9 @@ dependencies = [ [[package]] name = "windmill-api-groups" -version = "1.697.0" +version = "1.700.2" dependencies = [ - "axum 0.8.4", + "axum 0.8.9", "chrono", "globset", "lazy_static", @@ -16478,9 +14247,9 @@ dependencies = [ [[package]] name = "windmill-api-inputs" -version = "1.697.0" +version = "1.700.2" dependencies = [ - "axum 0.8.4", + "axum 0.8.9", "chrono", "serde", "serde_json", @@ -16492,14 +14261,14 @@ dependencies = [ [[package]] name = "windmill-api-integration-tests" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-nats", "aws-config", "aws-credential-types", "aws-sdk-sqs", - "axum 0.8.4", + "axum 0.8.9", "base64 0.22.1", "futures", "rand 0.9.0", @@ -16524,10 +14293,10 @@ dependencies = [ [[package]] name = "windmill-api-jobs" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", - "axum 0.8.4", + "axum 0.8.9", "base64 0.22.1", "chrono", "http 1.4.0", @@ -16549,9 +14318,9 @@ dependencies = [ [[package]] name = "windmill-api-npm-proxy" -version = "1.697.0" +version = "1.700.2" dependencies = [ - "axum 0.8.4", + "axum 0.8.9", "flate2", "reqwest 0.13.1", "serde", @@ -16567,10 +14336,10 @@ dependencies = [ [[package]] name = "windmill-api-openapi" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", - "axum 0.8.4", + "axum 0.8.9", "http 1.4.0", "indexmap 2.14.0", "itertools 0.14.0", @@ -16589,9 +14358,9 @@ dependencies = [ [[package]] name = "windmill-api-schedule" -version = "1.697.0" +version = "1.700.2" dependencies = [ - "axum 0.8.4", + "axum 0.8.9", "chrono", "chrono-tz", "serde", @@ -16609,9 +14378,9 @@ dependencies = [ [[package]] name = "windmill-api-scripts" -version = "1.697.0" +version = "1.700.2" dependencies = [ - "axum 0.8.4", + "axum 0.8.9", "chrono", "futures", "http 1.4.0", @@ -16639,10 +14408,10 @@ dependencies = [ [[package]] name = "windmill-api-settings" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", - "axum 0.8.4", + "axum 0.8.9", "base64 0.22.1", "bytes", "chrono", @@ -16667,7 +14436,7 @@ dependencies = [ [[package]] name = "windmill-api-sse" -version = "1.697.0" +version = "1.700.2" dependencies = [ "lazy_static", "serde", @@ -16679,12 +14448,12 @@ dependencies = [ [[package]] name = "windmill-api-users" -version = "1.697.0" +version = "1.700.2" dependencies = [ "argon2", - "axum 0.8.4", + "axum 0.8.9", "chrono", - "dashmap 6.1.0", + "dashmap", "http 1.4.0", "hyper 1.9.0", "lazy_static", @@ -16704,9 +14473,9 @@ dependencies = [ [[package]] name = "windmill-api-workers" -version = "1.697.0" +version = "1.700.2" dependencies = [ - "axum 0.8.4", + "axum 0.8.9", "chrono", "serde", "serde_json", @@ -16718,9 +14487,9 @@ dependencies = [ [[package]] name = "windmill-api-workspaces" -version = "1.697.0" +version = "1.700.2" dependencies = [ - "axum 0.8.4", + "axum 0.8.9", "chrono", "hex", "http 1.4.0", @@ -16751,7 +14520,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.697.0" +version = "1.700.2" dependencies = [ "chrono", "lazy_static", @@ -16765,10 +14534,10 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", - "axum 0.8.4", + "axum 0.8.9", "k8s-openapi", "kube", "serde", @@ -16784,7 +14553,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.697.0" +version = "1.700.2" dependencies = [ "aes-gcm", "aho-corasick", @@ -16798,10 +14567,10 @@ dependencies = [ "aws-sdk-secretsmanager", "aws-sdk-sts", "aws-smithy-types-convert", - "axum 0.8.4", + "axum 0.8.9", "backon", "base64 0.22.1", - "bitflags 2.9.4", + "bitflags 2.11.1", "bytes", "chrono", "chrono-tz", @@ -16810,7 +14579,7 @@ dependencies = [ "crc", "cron", "croner", - "dashmap 6.1.0", + "dashmap", "datafusion", "equivalent", "futures", @@ -16819,7 +14588,7 @@ dependencies = [ "git-version", "globset", "hex", - "hmac 0.12.1", + "hmac", "hyper 1.9.0", "indexmap 2.14.0", "itertools 0.14.0", @@ -16838,7 +14607,7 @@ dependencies = [ "pep440_rs", "phf 0.11.3", "pin-project-lite", - "postgres-native-tls 0.5.1", + "postgres-native-tls 0.5.3", "prometheus", "quick_cache", "rand 0.9.0", @@ -16864,7 +14633,7 @@ dependencies = [ "thiserror 2.0.18", "tikv-jemalloc-ctl", "tokio", - "tokio-postgres 0.7.13", + "tokio-postgres", "tokio-stream", "tokio-util", "tonic 0.13.1", @@ -16885,7 +14654,7 @@ dependencies = [ [[package]] name = "windmill-dep-map" -version = "1.697.0" +version = "1.700.2" dependencies = [ "chrono", "itertools 0.14.0", @@ -16904,7 +14673,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.697.0" +version = "1.700.2" dependencies = [ "regex", "serde", @@ -16919,7 +14688,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "astral-tokio-tar", @@ -16943,7 +14712,7 @@ dependencies = [ [[package]] name = "windmill-jseval" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "futures", @@ -16960,7 +14729,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.697.0" +version = "1.700.2" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -16976,7 +14745,7 @@ dependencies = [ [[package]] name = "windmill-mcp" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-trait", @@ -16997,15 +14766,15 @@ dependencies = [ [[package]] name = "windmill-native-triggers" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-trait", - "axum 0.8.4", + "axum 0.8.9", "backon", "base64 0.22.1", "chrono", - "hmac 0.12.1", + "hmac", "http 1.4.0", "itertools 0.14.0", "lazy_static", @@ -17028,16 +14797,16 @@ dependencies = [ [[package]] name = "windmill-oauth" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "arc-swap", "async-oauth2", - "axum 0.8.4", + "axum 0.8.9", "base64 0.22.1", "chrono", "hex", - "hmac 0.12.1", + "hmac", "itertools 0.14.0", "lazy_static", "reqwest 0.12.28", @@ -17053,7 +14822,7 @@ dependencies = [ [[package]] name = "windmill-object-store" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-stream", @@ -17062,7 +14831,7 @@ dependencies = [ "aws-credential-types", "aws-sdk-sts", "aws-smithy-types-convert", - "axum 0.8.4", + "axum 0.8.9", "bytes", "chrono", "datafusion", @@ -17087,7 +14856,7 @@ dependencies = [ [[package]] name = "windmill-operator" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "futures", @@ -17105,7 +14874,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.697.0" +version = "1.700.2" dependencies = [ "convert_case 0.6.0", "serde", @@ -17114,7 +14883,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "lazy_static", @@ -17126,7 +14895,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "serde_json", @@ -17138,7 +14907,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "gosyn", @@ -17150,7 +14919,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "lazy_static", @@ -17162,7 +14931,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "serde_json", @@ -17174,7 +14943,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "nu-parser", @@ -17185,7 +14954,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "itertools 0.14.0", @@ -17196,7 +14965,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "itertools 0.14.0", @@ -17208,7 +14977,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "rustpython-ast", @@ -17219,7 +14988,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-recursion", @@ -17241,7 +15010,7 @@ dependencies = [ [[package]] name = "windmill-parser-r" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "serde_json", @@ -17253,7 +15022,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "lazy_static", @@ -17267,7 +15036,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -17284,7 +15053,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "lazy_static", @@ -17297,7 +15066,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "serde", @@ -17309,7 +15078,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "lazy_static", @@ -17327,7 +15096,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "serde-wasm-bindgen", @@ -17343,7 +15112,7 @@ dependencies = [ [[package]] name = "windmill-parser-wac" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "rustpython-ast", @@ -17359,7 +15128,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "serde", @@ -17370,20 +15139,20 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-recursion", - "axum 0.8.4", + "axum 0.8.9", "backon", "chrono", "chrono-tz", "cron", - "dashmap 6.1.0", + "dashmap", "futures", "futures-core", "hex", - "hmac 0.12.1", + "hmac", "itertools 0.14.0", "lazy_static", "once_cell", @@ -17407,19 +15176,19 @@ dependencies = [ [[package]] name = "windmill-runtime-nativets" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "const_format", "deno_ast", "deno_console", "deno_core", - "deno_error", + "deno_error 0.6.1", "deno_fetch", + "deno_fs", "deno_io", "deno_net", "deno_permissions", - "deno_runtime", "deno_telemetry", "deno_tls", "deno_url", @@ -17445,7 +15214,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.697.0" +version = "1.700.2" dependencies = [ "getrandom 0.3.4", "wasm-bindgen", @@ -17456,11 +15225,11 @@ dependencies = [ [[package]] name = "windmill-store" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-recursion", - "axum 0.8.4", + "axum 0.8.9", "chrono", "futures", "http 1.4.0", @@ -17486,11 +15255,11 @@ dependencies = [ [[package]] name = "windmill-test-utils" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-trait", - "axum 0.8.4", + "axum 0.8.9", "chrono", "futures", "serde", @@ -17510,11 +15279,11 @@ dependencies = [ [[package]] name = "windmill-trigger" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-trait", - "axum 0.8.4", + "axum 0.8.9", "chrono", "http 1.4.0", "hyper 1.9.0", @@ -17543,11 +15312,11 @@ dependencies = [ [[package]] name = "windmill-trigger-azure" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-trait", - "axum 0.8.4", + "axum 0.8.9", "base64 0.22.1", "bytes", "chrono", @@ -17576,11 +15345,11 @@ dependencies = [ [[package]] name = "windmill-trigger-email" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-trait", - "axum 0.8.4", + "axum 0.8.9", "base64 0.22.1", "lazy_static", "regex", @@ -17596,11 +15365,11 @@ dependencies = [ [[package]] name = "windmill-trigger-gcp" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-trait", - "axum 0.8.4", + "axum 0.8.9", "base64 0.22.1", "bytes", "chrono", @@ -17630,17 +15399,17 @@ dependencies = [ [[package]] name = "windmill-trigger-http" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-trait", - "axum 0.8.4", + "axum 0.8.9", "base64 0.22.1", "chrono", "constant_time_eq 0.3.1", "futures", "hex", - "hmac 0.12.1", + "hmac", "http 1.4.0", "hyper 1.9.0", "itertools 0.14.0", @@ -17666,11 +15435,11 @@ dependencies = [ [[package]] name = "windmill-trigger-kafka" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-trait", - "axum 0.8.4", + "axum 0.8.9", "base64 0.22.1", "itertools 0.14.0", "rdkafka", @@ -17689,11 +15458,11 @@ dependencies = [ [[package]] name = "windmill-trigger-mqtt" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-trait", - "axum 0.8.4", + "axum 0.8.9", "base64 0.22.1", "bytes", "itertools 0.14.0", @@ -17713,12 +15482,12 @@ dependencies = [ [[package]] name = "windmill-trigger-nats" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-nats", "async-trait", - "axum 0.8.4", + "axum 0.8.9", "base64 0.22.1", "itertools 0.14.0", "nkeys", @@ -17737,11 +15506,11 @@ dependencies = [ [[package]] name = "windmill-trigger-postgres" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-trait", - "axum 0.8.4", + "axum 0.8.9", "byteorder", "bytes", "chrono", @@ -17750,7 +15519,7 @@ dependencies = [ "lazy_static", "native-tls", "pg_escape", - "postgres-native-tls 0.5.0", + "postgres-native-tls 0.5.2", "quick_cache", "rand 0.9.0", "rust_decimal", @@ -17759,7 +15528,7 @@ dependencies = [ "sqlx", "thiserror 2.0.18", "tokio", - "tokio-postgres 0.7.11", + "tokio-postgres", "tokio-stream", "tracing", "uuid", @@ -17772,7 +15541,7 @@ dependencies = [ [[package]] name = "windmill-trigger-sqs" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-trait", @@ -17781,7 +15550,7 @@ dependencies = [ "aws-sdk-sqs", "aws-sdk-sts", "aws-smithy-types", - "axum 0.8.4", + "axum 0.8.9", "backon", "chrono", "itertools 0.14.0", @@ -17800,11 +15569,11 @@ dependencies = [ [[package]] name = "windmill-trigger-websocket" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-trait", - "axum 0.8.4", + "axum 0.8.9", "futures", "http 1.4.0", "itertools 0.14.0", @@ -17823,10 +15592,10 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", - "bitflags 2.9.4", + "bitflags 2.11.1", "chrono", "hex", "itertools 0.14.0", @@ -17842,18 +15611,14 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-once-cell", "async-recursion", "async-stream", "async-trait", - "aws-config", - "aws-credential-types", - "aws-sdk-bedrockruntime", - "aws-smithy-types", - "axum 0.8.4", + "axum 0.8.9", "backon", "base64 0.22.1", "bit-vec 0.6.3", @@ -17870,7 +15635,7 @@ dependencies = [ "gcp_auth", "git-version", "hex", - "hmac 0.12.1", + "hmac", "hudsucker", "hyper-http-proxy", "hyper-tls", @@ -17879,7 +15644,7 @@ dependencies = [ "jsonwebtoken 8.3.0", "lazy_static", "libffi-sys", - "libloading 0.8.9", + "libloading", "mappable-rc", "mime_guess", "mysql_async", @@ -17891,10 +15656,11 @@ dependencies = [ "oracle", "pem 3.0.6", "pep440_rs", - "postgres-native-tls 0.5.1", + "postgres-native-tls 0.5.3", "process-wrap", "prometheus", "prost", + "quick_cache", "rand 0.9.0", "rcgen", "regex", @@ -17909,7 +15675,7 @@ dependencies = [ "tempfile", "tiberius", "tokio", - "tokio-postgres 0.7.13", + "tokio-postgres", "tokio-stream", "tokio-util", "tracing", @@ -17948,13 +15714,13 @@ dependencies = [ "windmill-types", "windmill-worker-volumes", "windows 0.61.3", - "x509-parser 0.16.0", + "x509-parser", "yaml-rust", ] [[package]] name = "windmill-worker-volumes" -version = "1.697.0" +version = "1.700.2" dependencies = [ "bytes", "futures", @@ -18205,7 +15971,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24d6bcc7f734a4091ecf8d7a64c5f7d7066f45585c1861eba06449909609c8a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "widestring", "windows-sys 0.52.0", ] @@ -18557,6 +16323,9 @@ name = "winnow" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] [[package]] name = "winsafe" @@ -18628,7 +16397,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.9.4", + "bitflags 2.11.1", "indexmap 2.14.0", "log", "serde", @@ -18664,12 +16433,6 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" -[[package]] -name = "wtf8" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c01ae8492c38f52376efd3a17d0994b6bcf3df1e39c0226d458b7d81670b2a06" - [[package]] name = "wyz" version = "0.5.1" @@ -18679,47 +16442,18 @@ dependencies = [ "tap", ] -[[package]] -name = "x25519-dalek" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" -dependencies = [ - "curve25519-dalek", - "rand_core 0.6.4", - "serde", - "zeroize", -] - -[[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 7.1.3", - "oid-registry 0.6.1", - "rusticata-macros", - "thiserror 1.0.69", - "time", -] - [[package]] name = "x509-parser" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" dependencies = [ - "asn1-rs 0.6.2", + "asn1-rs", "data-encoding", - "der-parser 9.0.0", + "der-parser", "lazy_static", - "nom 7.1.3", - "oid-registry 0.7.1", + "nom", + "oid-registry", "ring 0.17.14", "rusticata-macros", "thiserror 1.0.69", @@ -18736,12 +16470,6 @@ dependencies = [ "rustix 1.1.4", ] -[[package]] -name = "xml-rs" -version = "0.8.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" - [[package]] name = "xmlparser" version = "0.13.6" @@ -18781,18 +16509,6 @@ dependencies = [ "time", ] -[[package]] -name = "yoke" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive 0.7.5", - "zerofrom", -] - [[package]] name = "yoke" version = "0.8.2" @@ -18800,22 +16516,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", - "yoke-derive 0.8.2", + "yoke-derive", "zerofrom", ] -[[package]] -name = "yoke-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure 0.13.2", -] - [[package]] name = "yoke-derive" version = "0.8.2" @@ -18825,7 +16529,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.117", - "synstructure 0.13.2", + "synstructure", ] [[package]] @@ -18850,9 +16554,9 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] @@ -18866,7 +16570,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.117", - "synstructure 0.13.2", + "synstructure", ] [[package]] @@ -18874,20 +16578,6 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] [[package]] name = "zerotrie" @@ -18896,7 +16586,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", - "yoke 0.8.2", + "yoke", "zerofrom", ] @@ -18906,7 +16596,7 @@ version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ - "yoke 0.8.2", + "yoke", "zerofrom", "zerovec-derive", ] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 0d25909c74..0babfe3810 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.697.0" +version = "1.700.2" authors.workspace = true edition.workspace = true @@ -87,7 +87,7 @@ members = [ exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"] [workspace.package] -version = "1.697.0" +version = "1.700.2" authors = ["Ruben Fiszel "] edition = "2021" @@ -207,6 +207,36 @@ all_sqlx_features = ["all_languages", "enterprise", "enterprise_saml", "embeddin object_store = { git = "https://github.com/apache/arrow-rs-object-store", rev = "36752c975d4f29e20b57c91f81a10872dcd48ae7" } # Use tiberius main branch for libgssapi 0.8.1 fix (https://github.com/prisma/tiberius/issues/343) tiberius = { git = "https://github.com/prisma/tiberius", rev = "59db57960a14b422fb3a1309aa4aa47880896ff8" } +# Pin tokio-postgres / postgres-types / postgres-protocol to the +# MaterializeInc fork. windmill-trigger-postgres already pulled this +# fork in transitively for the postgres-replication crate +# (CopyBothDuplex, LogicalReplicationStream, TupleData with binary +# tuple support) which upstream rust-postgres has declined to merge +# since 2021 (PR #752 → #778, both still unmerged). +# +# MI also carries a mitigation for the +# Client::query_typed_raw / Client::prepare deadlock on result columns +# whose Oid the client doesn't know about yet (citext, custom enums / +# domains, postgis): MI's 2025-12-11 PR #33 resized the per-request +# response channel from mpsc::channel(1) → mpsc::channel(1024). +# bounded(1024) is sufficient for any realistic typeinfo deferral +# (need ~2-3 batches) but leaves a theoretical failure mode at +# >~64 MB results with a custom-Oid column. The strict-correct fix is +# mpsc::unbounded(); a follow-up PR to MI is open proposing that. +# +# The [patch.crates-io] entries below force windmill-worker's +# pg_executor (which imports `tokio_postgres::` directly from +# crates.io) onto the same fork as windmill-trigger-postgres, so the +# deadlock mitigation reaches both consumers. +# +# Upstream deadlock PRs (open, not on the critical path now that MI +# is mitigated): +# https://github.com/rust-postgres/rust-postgres/pull/1348 +# https://github.com/rust-postgres/rust-postgres/pull/1349 +# Reproducer: https://github.com/rubenfiszel/tokio-postgres-deadlock-repro +tokio-postgres = { git = "https://github.com/MaterializeInc/rust-postgres", rev = "78c1222577bb091d69bc22b1bc7ad01c14675abe" } +postgres-types = { git = "https://github.com/MaterializeInc/rust-postgres", rev = "78c1222577bb091d69bc22b1bc7ad01c14675abe" } +postgres-protocol = { git = "https://github.com/MaterializeInc/rust-postgres", rev = "78c1222577bb091d69bc22b1bc7ad01c14675abe" } [dependencies] anyhow.workspace = true @@ -387,8 +417,7 @@ tokio-stream = { version = "0.1.17" } tower = "^0" tower-http = { version = "^0.6", features = ["trace", "cors", "catch-panic"] } tower-cookies = "^0.11" -#stuck because of swc for now -serde = "=1.0.220" +serde = "^1" serde_json = { version = "^1", features = ["preserve_order", "raw_value"] } serde_yml = "0.0.12" uuid = { version = "^1", features = ["serde", "v4", "js"] } @@ -443,21 +472,29 @@ aws-sdk-rds = "^1" async-trait = "0.1.88" -v8 = "=130.0.7" # Exact version NOTE: Do not forget to update version and hash in flake.nix -deno_fetch = "0.214.0" -deno_tls = "0.177.0" -deno_console = "0.190.0" -deno_url = "0.190.0" -deno_webidl = "0.190.0" -deno_web = "0.221.0" -deno_io = "0.100.0" -deno_net = "0.182.0" -deno_core = "0.336.0" -deno_ast = { version = "=0.44.0", features = ["transpiling"] } -deno_permissions = "0.49.0" -deno_runtime = { version = "0.198.0", features = ["transpile"] } -deno_telemetry = "0.12.0" -deno_error = "=0.5.5" +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" +deno_io = "0.119.0" +deno_fs = "0.119.0" +deno_net = "0.201.0" +deno_core = "0.352.0" +deno_ast = { version = "=0.51.0", features = ["transpiling"] } +deno_permissions = "0.68.0" +deno_telemetry = "0.31.0" +deno_error = "=0.6.1" rustls-pemfile = "2.2.0" # only used with special deno_core_mac feature to prevent ffi issue on macos, requires libffi to be installed @@ -470,10 +507,10 @@ google-cloud-googleapis = {version = "0.16.1", features = ["pubsub"]} winapi = { version = "0.3.9", features = ["sysinfoapi"] } sysinfo = { version = "0.32.1" } -swc_common = "=0.37.5" -swc_ecma_parser = "=0.149.1" -swc_ecma_ast = "=0.118.2" -swc_ecma_visit = "=0.104.8" +swc_common = "=14.0.4" +swc_ecma_parser = "=24.0.3" +swc_ecma_ast = "=15.0.0" +swc_ecma_visit = "=15.0.0" async-recursion = "^1" @@ -517,8 +554,8 @@ wasm-bindgen-test = "^0" convert_case = "0.6.0" getrandom = "0.2" tokio-postgres = {version = "^0.7", features = ["array-impls", "with-serde_json-1", "with-chrono-0_4", "with-uuid-1", "with-bit-vec-0_6"]} -rust-postgres = { package = "tokio-postgres", git = "https://github.com/imor/rust-postgres", rev = "20265ef38e32a06f76b6f9b678e2077fc2211f6b"} -rust-postgres-native-tls = { package = "postgres-native-tls", git = "https://github.com/imor/rust-postgres", features = ["runtime"], rev = "20265ef38e32a06f76b6f9b678e2077fc2211f6b" } +rust-postgres = { package = "tokio-postgres", git = "https://github.com/MaterializeInc/rust-postgres", rev = "78c1222577bb091d69bc22b1bc7ad01c14675abe"} +rust-postgres-native-tls = { package = "postgres-native-tls", git = "https://github.com/MaterializeInc/rust-postgres", features = ["runtime"], rev = "78c1222577bb091d69bc22b1bc7ad01c14675abe" } bit-vec = "=0.6.3" mappable-rc = "^0" mysql_async = { version = "*", default-features = false, features = ["minimal", "default", "native-tls-tls", "rust_decimal"]} diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 401a8213f7..8a34b69a0e 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -c8d100d74b8de6bd26fc973d5edbd8853d54dd8b +f9494c6320bb5fd07c1e9e09734b7fd5fbe7aa38 diff --git a/backend/migrations/20260511075225_add_assets_to_operator_settings.down.sql b/backend/migrations/20260511075225_add_assets_to_operator_settings.down.sql new file mode 100644 index 0000000000..c38c2a1e42 --- /dev/null +++ b/backend/migrations/20260511075225_add_assets_to_operator_settings.down.sql @@ -0,0 +1,19 @@ +-- Remove "assets" key from operator_settings +UPDATE workspace_settings +SET operator_settings = operator_settings - 'assets' +WHERE operator_settings IS NOT NULL + AND operator_settings ? 'assets'; + +-- Revert the column default +ALTER TABLE workspace_settings +ALTER COLUMN operator_settings SET DEFAULT '{ + "runs": true, + "groups": true, + "folders": true, + "workers": true, + "triggers": true, + "resources": true, + "schedules": true, + "variables": true, + "audit_logs": true +}'; diff --git a/backend/migrations/20260511075225_add_assets_to_operator_settings.up.sql b/backend/migrations/20260511075225_add_assets_to_operator_settings.up.sql new file mode 100644 index 0000000000..96aba860ec --- /dev/null +++ b/backend/migrations/20260511075225_add_assets_to_operator_settings.up.sql @@ -0,0 +1,21 @@ +-- Add "assets": true to operator_settings for all workspaces that have operator_settings +-- but don't already have an "assets" key +UPDATE workspace_settings +SET operator_settings = operator_settings || '{"assets": true}'::jsonb +WHERE operator_settings IS NOT NULL + AND NOT operator_settings ? 'assets'; + +-- Update the column default to include assets +ALTER TABLE workspace_settings +ALTER COLUMN operator_settings SET DEFAULT '{ + "runs": true, + "groups": true, + "folders": true, + "workers": true, + "triggers": true, + "resources": true, + "schedules": true, + "variables": true, + "audit_logs": true, + "assets": true +}'; diff --git a/backend/parsers/windmill-parser-ts-asset/src/lib.rs b/backend/parsers/windmill-parser-ts-asset/src/lib.rs index bc31a16c99..3c04ef7447 100644 --- a/backend/parsers/windmill-parser-ts-asset/src/lib.rs +++ b/backend/parsers/windmill-parser-ts-asset/src/lib.rs @@ -12,7 +12,7 @@ use AssetUsageAccessType::*; pub fn parse_assets(code: &str) -> anyhow::Result { let cm: Lrc = Default::default(); - let fm = cm.new_source_file(FileName::Custom("main.ts".into()).into(), code.into()); + let fm = cm.new_source_file(FileName::Custom("main.ts".into()).into(), code.to_string()); let lexer = Lexer::new( // We want to parse ecmascript Syntax::Typescript(TsSyntax::default()), diff --git a/backend/parsers/windmill-parser-ts/src/lib.rs b/backend/parsers/windmill-parser-ts/src/lib.rs index 1ae22879b6..1e78ec8665 100644 --- a/backend/parsers/windmill-parser-ts/src/lib.rs +++ b/backend/parsers/windmill-parser-ts/src/lib.rs @@ -129,7 +129,7 @@ impl Visit for ImportsFinder { /// See also: [`parse_relative_imports`] for resolved absolute paths. pub fn parse_expr_for_imports(code: &str, skip_type_only: bool) -> anyhow::Result> { let cm: Lrc = Default::default(); - let fm = cm.new_source_file(FileName::Custom("main.d.ts".into()).into(), code.into()); + let fm = cm.new_source_file(FileName::Custom("main.d.ts".into()).into(), code.to_string()); let mut tss = TsSyntax::default(); tss.disallow_ambiguous_jsx_like; tss.tsx = true; @@ -263,7 +263,7 @@ impl Visit for OutputFinder { pub fn parse_expr_for_ids(code: &str) -> anyhow::Result> { let cm: Lrc = Default::default(); - let fm = cm.new_source_file(FileName::Custom("main.ts".into()).into(), code.into()); + let fm = cm.new_source_file(FileName::Custom("main.ts".into()).into(), code.to_string()); let lexer = Lexer::new( // We want to parse ecmascript Syntax::Es(EsSyntax { jsx: false, ..Default::default() }), @@ -305,7 +305,7 @@ pub fn parse_deno_signature( entrypoint_override: Option, ) -> anyhow::Result { let cm: Lrc = Default::default(); - let fm = cm.new_source_file(FileName::Custom("main.ts".into()).into(), code.into()); + let fm = cm.new_source_file(FileName::Custom("main.ts".into()).into(), code.to_string()); let lexer = Lexer::new( // We want to parse ecmascript Syntax::Typescript(TsSyntax::default()), diff --git a/backend/parsers/windmill-parser-wac/src/typescript.rs b/backend/parsers/windmill-parser-wac/src/typescript.rs index 87fa7cef01..9ad84a2e97 100644 --- a/backend/parsers/windmill-parser-wac/src/typescript.rs +++ b/backend/parsers/windmill-parser-wac/src/typescript.rs @@ -712,7 +712,7 @@ fn extract_ts_params(params: &[swc_ecma_ast::Param], cm: &Lrc) -> Vec pub fn parse_ts_workflow(code: &str) -> Result> { let cm: Lrc = Default::default(); - let fm = cm.new_source_file(FileName::Custom("workflow.ts".into()).into(), code.into()); + let fm = cm.new_source_file(FileName::Custom("workflow.ts".into()).into(), code.to_string()); let lexer = Lexer::new( Syntax::Typescript(TsSyntax::default()), Default::default(), diff --git a/backend/parsers/windmill-parser-wasm/Cargo.lock b/backend/parsers/windmill-parser-wasm/Cargo.lock index 4bb0074734..954303108f 100644 --- a/backend/parsers/windmill-parser-wasm/Cargo.lock +++ b/backend/parsers/windmill-parser-wasm/Cargo.lock @@ -6183,7 +6183,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windmill-common" -version = "1.697.0" +version = "1.700.2" dependencies = [ "aho-corasick", "anyhow", @@ -6263,7 +6263,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.697.0" +version = "1.700.2" dependencies = [ "proc-macro2", "quote", @@ -6275,7 +6275,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.697.0" +version = "1.700.2" dependencies = [ "convert_case", "serde", @@ -6284,7 +6284,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "lazy_static", @@ -6296,7 +6296,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "serde_json", @@ -6308,7 +6308,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "gosyn", @@ -6320,7 +6320,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "lazy_static", @@ -6332,7 +6332,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "serde_json", @@ -6344,7 +6344,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "nu-parser", @@ -6355,7 +6355,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "itertools 0.14.0", @@ -6366,7 +6366,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "itertools 0.14.0", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "rustpython-ast", @@ -6389,7 +6389,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "async-recursion", @@ -6411,7 +6411,7 @@ dependencies = [ [[package]] name = "windmill-parser-r" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "serde_json", @@ -6423,7 +6423,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "lazy_static", @@ -6437,7 +6437,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "convert_case", @@ -6454,7 +6454,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "lazy_static", @@ -6467,7 +6467,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "serde", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "lazy_static", @@ -6497,7 +6497,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "serde-wasm-bindgen", @@ -6513,7 +6513,7 @@ dependencies = [ [[package]] name = "windmill-parser-wac" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "rustpython-ast", @@ -6529,7 +6529,7 @@ dependencies = [ [[package]] name = "windmill-parser-wasm" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "getrandom 0.2.17", @@ -6561,7 +6561,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "serde", @@ -6572,7 +6572,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.697.0" +version = "1.700.2" dependencies = [ "anyhow", "bitflags", diff --git a/backend/parsers/windmill-parser-wasm/Cargo.toml b/backend/parsers/windmill-parser-wasm/Cargo.toml index e9dc39abc7..0131c16089 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.697.0" +version = "1.700.2" edition = "2021" authors = ["Ruben Fiszel "] diff --git a/backend/src/ee_oss.rs b/backend/src/ee_oss.rs index 2aefaf7431..4dcaf2437b 100644 --- a/backend/src/ee_oss.rs +++ b/backend/src/ee_oss.rs @@ -8,6 +8,6 @@ pub async fn set_license_key(_license_key: String, _db: Option<&windmill_common: } #[cfg(all(feature = "enterprise", not(feature = "private")))] -pub async fn verify_license_key() -> () { +pub async fn verify_license_key(_db: Option<&windmill_common::db::DB>) -> () { // Implementation is not open source } diff --git a/backend/src/main.rs b/backend/src/main.rs index 3049064624..9a2709e9a9 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1441,7 +1441,7 @@ Windmill Community Edition {GIT_VERSION} tracing::error!("Failed to reload license key on agent: {e:#}"); } #[cfg(feature = "enterprise")] - ee_oss::verify_license_key().await; + ee_oss::verify_license_key(conn.as_sql()).await; } // update min version explicitly. diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index 59a2f47ed0..0ddc02b256 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -2373,7 +2373,19 @@ pub async fn monitor_db( let verify_license_key_f = async { #[cfg(feature = "enterprise")] if !initial_load { - verify_license_key().await; + verify_license_key(conn.as_sql()).await; + } + }; + + let enforce_offline_caps_f = async { + #[cfg(feature = "enterprise")] + if server_mode && !initial_load { + if let Some(db) = conn.as_sql() { + // Cheap: one query for workers active in the last 2 minutes. + if let Err(e) = windmill_common::ee_oss::enforce_offline_caps(db).await { + tracing::error!("Failed to enforce offline license caps: {e:#}"); + } + } } }; @@ -2522,6 +2534,7 @@ pub async fn monitor_db( vacuum_queue_f, expose_queue_metrics_f, verify_license_key_f, + enforce_offline_caps_f, worker_groups_alerts_f, jobs_waiting_alerts_f, low_disk_alerts_f, @@ -2853,6 +2866,11 @@ pub async fn reload_base_url_setting(conn: &Connection) -> error::Result<()> { IS_SECURE.store(is_secure, Ordering::Relaxed); + #[cfg(feature = "enterprise")] + { + crate::ee_oss::verify_license_key(conn.as_sql()).await; + } + Ok(()) } diff --git a/backend/windmill-ai/Cargo.toml b/backend/windmill-ai/Cargo.toml index 69cc0f181b..542c390cc0 100644 --- a/backend/windmill-ai/Cargo.toml +++ b/backend/windmill-ai/Cargo.toml @@ -21,6 +21,10 @@ windmill-mcp = { workspace = true, optional = true } async-trait.workspace = true base64.workspace = true +bytes.workspace = true +eventsource-stream.workspace = true +futures.workspace = true +mime_guess.workspace = true reqwest.workspace = true serde.workspace = true serde_json.workspace = true @@ -29,6 +33,8 @@ uuid.workspace = true lazy_static.workspace = true tracing.workspace = true tokio.workspace = true +tokio-stream.workspace = true +ulid.workspace = true # Bedrock (optional) aws-config = { workspace = true, optional = true } diff --git a/backend/windmill-ai/src/ai_bedrock.rs b/backend/windmill-ai/src/ai_bedrock.rs index 49166978e8..c05094b8f4 100644 --- a/backend/windmill-ai/src/ai_bedrock.rs +++ b/backend/windmill-ai/src/ai_bedrock.rs @@ -326,8 +326,10 @@ pub fn json_to_document(value: serde_json::Value) -> aws_smithy_types::Document } Value::Array(arr) => Document::Array(arr.into_iter().map(json_to_document).collect()), Value::Number(num) => { - if let Some(i) = num.as_i64() { - Document::Number(aws_smithy_types::Number::PosInt(i as u64)) + if let Some(u) = num.as_u64() { + Document::Number(aws_smithy_types::Number::PosInt(u)) + } else if let Some(i) = num.as_i64() { + Document::Number(aws_smithy_types::Number::NegInt(i)) } else if let Some(f) = num.as_f64() { Document::Number(aws_smithy_types::Number::Float(f)) } else { @@ -844,6 +846,36 @@ mod tests { } } + #[test] + fn json_to_document_preserves_negative_integers() { + let value = serde_json::json!(-1); + let doc = json_to_document(value); + assert!(matches!( + doc, + aws_smithy_types::Document::Number(aws_smithy_types::Number::NegInt(-1)) + )); + } + + #[test] + fn json_to_document_handles_large_u64_above_i64_max() { + let value = serde_json::json!(u64::MAX); + let doc = json_to_document(value); + assert!(matches!( + doc, + aws_smithy_types::Document::Number(aws_smithy_types::Number::PosInt(u)) if u == u64::MAX + )); + } + + #[test] + fn json_to_document_handles_positive_integers() { + let value = serde_json::json!(42); + let doc = json_to_document(value); + assert!(matches!( + doc, + aws_smithy_types::Document::Number(aws_smithy_types::Number::PosInt(42)) + )); + } + #[test] fn openai_messages_to_bedrock_adds_cache_points_when_enabled() { let messages = vec![ diff --git a/backend/windmill-worker/src/ai/image_handler.rs b/backend/windmill-ai/src/image_handler.rs similarity index 86% rename from backend/windmill-worker/src/ai/image_handler.rs rename to backend/windmill-ai/src/image_handler.rs index 4fc0bcb4db..5bd9794da5 100644 --- a/backend/windmill-worker/src/ai/image_handler.rs +++ b/backend/windmill-ai/src/image_handler.rs @@ -1,16 +1,18 @@ +use crate::types::*; use base64::Engine; use futures; use ulid; +use uuid::Uuid; use windmill_common::{client::AuthedClient, error::Error}; -use windmill_queue::MiniPulledJob; use windmill_types::s3::S3Object; -use crate::ai::types::*; - -/// Upload image to S3 and return S3Object +/// Upload image to S3 and return S3Object. +/// +/// The caller must provide an AuthedClient authorized for `workspace_id`. pub async fn upload_image_to_s3( base64_image: &str, - job: &MiniPulledJob, + workspace_id: &str, + job_id: &Uuid, client: &AuthedClient, ) -> Result { let image_bytes = base64::engine::general_purpose::STANDARD @@ -19,7 +21,7 @@ pub async fn upload_image_to_s3( // Generate unique S3 key let unique_id = ulid::Ulid::new().to_string(); - let s3_key = format!("ai_images/{}/{}.png", job.id, unique_id); + let s3_key = format!("ai_images/{}/{}.png", job_id, unique_id); // Create byte stream let byte_stream = futures::stream::once(async move { @@ -29,7 +31,7 @@ pub async fn upload_image_to_s3( // Upload to S3 client .upload_s3_file( - &job.workspace_id, + workspace_id, s3_key.clone(), None, // storage - use default byte_stream, @@ -45,7 +47,9 @@ pub async fn upload_image_to_s3( }) } -/// Download an S3 image and convert it to a base64 data URL +/// Download an S3 image and convert it to a base64 data URL. +/// +/// The caller must provide an AuthedClient authorized for `workspace_id`. pub async fn download_and_encode_s3_image( image: &S3Object, client: &AuthedClient, @@ -71,6 +75,8 @@ pub async fn download_and_encode_s3_image( } /// Convert an S3Object to the appropriate ContentPart based on MIME type. +/// +/// The caller must provide an AuthedClient authorized for `workspace_id`. pub async fn s3_object_to_content_part( s3_object: &S3Object, client: &AuthedClient, @@ -80,7 +86,7 @@ pub async fn s3_object_to_content_part( download_and_encode_s3_image(s3_object, client, workspace_id).await?; let data_url = format!("data:{};base64,{}", mime_type, file_bytes); - if windmill_ai::ai_types::is_document_mime(&mime_type) { + if crate::ai_types::is_document_mime(&mime_type) { let filename = s3_object .s3 .rsplit('/') @@ -93,7 +99,9 @@ pub async fn s3_object_to_content_part( } } -/// Prepare messages for API by converting S3Objects to base64 ImageUrls +/// Prepare messages for API by converting S3Objects to base64 ImageUrls. +/// +/// The caller must provide an AuthedClient authorized for `workspace_id`. pub async fn prepare_messages_for_api( messages: &[OpenAIMessage], client: &AuthedClient, diff --git a/backend/windmill-ai/src/lib.rs b/backend/windmill-ai/src/lib.rs index dfa34e7d43..6b174bf2e6 100644 --- a/backend/windmill-ai/src/lib.rs +++ b/backend/windmill-ai/src/lib.rs @@ -4,5 +4,9 @@ pub mod ai_cache; pub mod ai_google; pub mod ai_providers; pub mod ai_types; +pub mod image_handler; +pub mod providers; pub mod query_builder; +pub mod sse; pub mod types; +pub mod utils; diff --git a/backend/windmill-worker/src/ai/providers/anthropic.rs b/backend/windmill-ai/src/providers/anthropic.rs similarity index 99% rename from backend/windmill-worker/src/ai/providers/anthropic.rs rename to backend/windmill-ai/src/providers/anthropic.rs index 2bbfd83768..43cb503d96 100644 --- a/backend/windmill-worker/src/ai/providers/anthropic.rs +++ b/backend/windmill-ai/src/providers/anthropic.rs @@ -1,16 +1,16 @@ -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; -use serde_json::value::RawValue; -use windmill_ai::{ai_google::parse_data_url, ai_providers::AIProvider}; -use windmill_common::{client::AuthedClient, error::Error}; - -use crate::ai::{ +use crate::{ + ai_google::parse_data_url, + ai_providers::AIProvider, image_handler::prepare_messages_for_api, query_builder::{BuildRequestArgs, ParsedResponse, QueryBuilder, StreamEventSink}, sse::{AnthropicSSEParser, SSEParser}, types::*, utils::{extract_text_content, should_use_structured_output_tool}, }; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; +use windmill_common::{client::AuthedClient, error::Error}; /// Anthropic API version for standard API const ANTHROPIC_VERSION_STANDARD: &str = "2023-06-01"; diff --git a/backend/windmill-worker/src/ai/providers/bedrock.rs b/backend/windmill-ai/src/providers/bedrock.rs similarity index 96% rename from backend/windmill-worker/src/ai/providers/bedrock.rs rename to backend/windmill-ai/src/providers/bedrock.rs index ce87d21a44..005327c15b 100644 --- a/backend/windmill-worker/src/ai/providers/bedrock.rs +++ b/backend/windmill-ai/src/providers/bedrock.rs @@ -1,4 +1,4 @@ -//! AWS Bedrock provider for the AI agent. +//! AWS Bedrock provider for AI requests. //! //! Uses shared SDK code from windmill_ai::ai_bedrock for: //! - BedrockClient (SDK wrapper with auth) @@ -6,28 +6,25 @@ //! - Stream event parsing //! - Helper utilities -use crate::ai::{ +use crate::{ image_handler::prepare_messages_for_api, query_builder::{ParsedResponse, StreamEventSink}, - types::StreamingEvent, - types::TokenUsage, - types::{OpenAIMessage, ToolDef}, + types::{OpenAIMessage, StreamingEvent, TokenUsage, ToolDef}, }; use std::collections::HashMap; use windmill_common::{client::AuthedClient, error::Error}; -// Re-export from shared module for use by other parts of the worker -use windmill_ai::ai_bedrock::{ +// Import shared Bedrock helpers for provider orchestration. +use crate::ai_bedrock::{ bedrock_model_supports_prompt_caching, bedrock_stream_event_is_block_stop, bedrock_stream_event_to_text, bedrock_stream_event_to_tool_delta, bedrock_stream_event_to_tool_start, build_tool_config, create_inference_config, format_bedrock_error, openai_messages_to_bedrock, streaming_tool_calls_to_openai, - StreamingToolCall, + BedrockClient, StreamingToolCall, }; -pub use windmill_ai::ai_bedrock::{check_env_credentials, BedrockClient}; // ============================================================================ -// Query Builder (Worker-specific orchestration) +// Query Builder // ============================================================================ #[derive(Default)] diff --git a/backend/windmill-worker/src/ai/providers/google_ai.rs b/backend/windmill-ai/src/providers/google_ai.rs similarity index 97% rename from backend/windmill-worker/src/ai/providers/google_ai.rs rename to backend/windmill-ai/src/providers/google_ai.rs index 6098cb22d8..81f34e7acb 100644 --- a/backend/windmill-worker/src/ai/providers/google_ai.rs +++ b/backend/windmill-ai/src/providers/google_ai.rs @@ -1,17 +1,16 @@ -use async_trait::async_trait; -use windmill_ai::ai_google::{ - openai_messages_to_gemini, openai_tools_to_gemini, GeminiGenerationConfig, GeminiImageContent, - GeminiImageRequest, GeminiImageResponse, GeminiInlineData, GeminiPart, GeminiPredictContent, - GeminiTextRequest, GeminiTool, -}; -use windmill_common::{client::AuthedClient, error::Error}; - -use crate::ai::{ +use crate::{ + ai_google::{ + openai_messages_to_gemini, openai_tools_to_gemini, GeminiGenerationConfig, + GeminiImageContent, GeminiImageRequest, GeminiImageResponse, GeminiInlineData, GeminiPart, + GeminiPredictContent, GeminiTextRequest, GeminiTool, + }, image_handler::{download_and_encode_s3_image, prepare_messages_for_api}, query_builder::{BuildRequestArgs, ParsedResponse, QueryBuilder, StreamEventSink}, sse::{GeminiSSEParser, SSEParser}, types::*, }; +use async_trait::async_trait; +use windmill_common::{client::AuthedClient, error::Error}; // ============================================================================ // Query Builder Implementation diff --git a/backend/windmill-ai/src/providers/mod.rs b/backend/windmill-ai/src/providers/mod.rs new file mode 100644 index 0000000000..8f9e2382a8 --- /dev/null +++ b/backend/windmill-ai/src/providers/mod.rs @@ -0,0 +1,31 @@ +pub mod anthropic; +#[cfg(feature = "bedrock")] +pub mod bedrock; +pub mod google_ai; +pub mod openai; +pub mod openrouter; +pub mod other; + +use crate::{ai_providers::AIProvider, query_builder::QueryBuilder, types::ProviderWithResource}; + +use self::{ + anthropic::AnthropicQueryBuilder, google_ai::GoogleAIQueryBuilder, openai::OpenAIQueryBuilder, + openrouter::OpenRouterQueryBuilder, other::OtherQueryBuilder, +}; + +/// Factory function to create the appropriate query builder for a provider. +pub fn create_query_builder(provider: &ProviderWithResource) -> Box { + match provider.kind { + AIProvider::GoogleAI => { + Box::new(GoogleAIQueryBuilder::new(provider.get_platform().clone())) + } + AIProvider::OpenAI => Box::new(OpenAIQueryBuilder::new(provider.kind.clone())), + AIProvider::Anthropic => Box::new(AnthropicQueryBuilder::new( + provider.kind.clone(), + provider.get_platform().clone(), + provider.get_enable_1m_context(), + )), + AIProvider::OpenRouter => Box::new(OpenRouterQueryBuilder::new()), + _ => Box::new(OtherQueryBuilder::new(provider.kind.clone())), + } +} diff --git a/backend/windmill-worker/src/ai/providers/openai.rs b/backend/windmill-ai/src/providers/openai.rs similarity index 99% rename from backend/windmill-worker/src/ai/providers/openai.rs rename to backend/windmill-ai/src/providers/openai.rs index 52feb2eca7..f8928ca940 100644 --- a/backend/windmill-worker/src/ai/providers/openai.rs +++ b/backend/windmill-ai/src/providers/openai.rs @@ -1,17 +1,16 @@ -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; -use serde_json::value::RawValue; -use windmill_ai::ai_providers::AIProvider; -use windmill_ai::ai_types::OpenAIToolCall; -use windmill_common::{client::AuthedClient, error::Error}; - -use crate::ai::{ +use crate::{ + ai_providers::AIProvider, + ai_types::OpenAIToolCall, image_handler::{prepare_messages_for_api, s3_object_to_content_part}, query_builder::{BuildRequestArgs, ParsedResponse, QueryBuilder, StreamEventSink}, sse::{OpenAIResponsesSSEParser, SSEParser}, types::*, utils::extract_text_content, }; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; +use windmill_common::{client::AuthedClient, error::Error}; // Responses API structures #[derive(Deserialize)] diff --git a/backend/windmill-worker/src/ai/providers/openrouter.rs b/backend/windmill-ai/src/providers/openrouter.rs similarity index 97% rename from backend/windmill-worker/src/ai/providers/openrouter.rs rename to backend/windmill-ai/src/providers/openrouter.rs index 68ad1922d3..aaefbb08ca 100644 --- a/backend/windmill-worker/src/ai/providers/openrouter.rs +++ b/backend/windmill-ai/src/providers/openrouter.rs @@ -1,15 +1,15 @@ -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; -use serde_json; -use windmill_ai::ai_providers::AIProvider; -use windmill_common::{client::AuthedClient, error::Error}; - -use crate::ai::{ +use crate::{ + ai_providers::AIProvider, image_handler::prepare_messages_for_api, - providers::other::OtherQueryBuilder, query_builder::{BuildRequestArgs, ParsedResponse, QueryBuilder, StreamEventSink}, types::*, }; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use serde_json; +use windmill_common::{client::AuthedClient, error::Error}; + +use crate::providers::other::OtherQueryBuilder; // OpenRouter-specific types #[derive(Serialize)] diff --git a/backend/windmill-worker/src/ai/providers/other.rs b/backend/windmill-ai/src/providers/other.rs similarity index 99% rename from backend/windmill-worker/src/ai/providers/other.rs rename to backend/windmill-ai/src/providers/other.rs index f8ee60f287..08f922a1de 100644 --- a/backend/windmill-worker/src/ai/providers/other.rs +++ b/backend/windmill-ai/src/providers/other.rs @@ -1,16 +1,15 @@ -use async_trait::async_trait; -use serde::Serialize; -use serde_json; -use windmill_ai::ai_providers::AIProvider; -use windmill_common::{client::AuthedClient, error::Error}; - -use crate::ai::{ +use crate::{ + ai_providers::AIProvider, image_handler::prepare_messages_for_api, query_builder::{BuildRequestArgs, ParsedResponse, QueryBuilder, StreamEventSink}, sse::{OpenAISSEParser, SSEParser}, types::*, utils::should_use_structured_output_tool, }; +use async_trait::async_trait; +use serde::Serialize; +use serde_json; +use windmill_common::{client::AuthedClient, error::Error}; #[derive(Serialize, Debug, Clone)] #[serde(rename_all = "lowercase")] diff --git a/backend/windmill-worker/src/ai/sse.rs b/backend/windmill-ai/src/sse.rs similarity index 99% rename from backend/windmill-worker/src/ai/sse.rs rename to backend/windmill-ai/src/sse.rs index 2cc03b148e..35a4e43acc 100644 --- a/backend/windmill-worker/src/ai/sse.rs +++ b/backend/windmill-ai/src/sse.rs @@ -3,17 +3,15 @@ use std::collections::HashMap; use eventsource_stream::Eventsource; use reqwest::Response; use serde::Deserialize; -use serde_json; use tokio_stream::StreamExt; -use windmill_ai::{ - ai_google::{parse_gemini_sse_event, GeminiUsageMetadata}, - ai_types::{ExtraContent, GoogleExtraContent, OpenAIFunction, OpenAIToolCall}, -}; use windmill_common::{error::Error, utils::rd_string}; -use crate::ai::{ +use crate::{ + ai_google::{parse_gemini_sse_event, GeminiUsageMetadata}, + ai_types::UrlCitation, + ai_types::{ExtraContent, GoogleExtraContent, OpenAIFunction, OpenAIToolCall}, query_builder::StreamEventSink, - types::{StreamingEvent, UrlCitation}, + types::StreamingEvent, }; #[derive(Deserialize)] @@ -64,6 +62,7 @@ lazy_static::lazy_static! { .parse::() .unwrap_or(false); } +#[allow(async_fn_in_trait)] pub trait SSEParser { async fn parse_event_data(&mut self, data: &str) -> Result<(), Error>; @@ -459,11 +458,11 @@ impl SSEParser for AnthropicSSEParser { // Gemini SSE Parser // ============================================================================ -/// Accumulates Gemini streaming events and converts them into the worker's -/// internal [`OpenAIToolCall`] / [`StreamingEvent`] representation. +/// Accumulates Gemini streaming events and converts them into the shared +/// [`OpenAIToolCall`] / [`StreamingEvent`] representation. /// /// The actual SSE parsing is delegated to [`parse_gemini_sse_event`] from -/// `windmill_common::ai_google` so the logic can be shared with the API proxy. +/// `windmill_ai::ai_google` so the logic can be shared with the API proxy. pub struct GeminiSSEParser { pub accumulated_content: String, pub accumulated_tool_calls: HashMap, diff --git a/backend/windmill-ai/src/utils.rs b/backend/windmill-ai/src/utils.rs new file mode 100644 index 0000000000..b401282df1 --- /dev/null +++ b/backend/windmill-ai/src/utils.rs @@ -0,0 +1,56 @@ +use crate::{ + ai_providers::AIProvider, + ai_types::{ContentPart, OpenAIContent}, +}; + +lazy_static::lazy_static! { + /// Parse AI_HTTP_HEADERS environment variable into a vector of (header_name, header_value) tuples + /// Format: "header1: value1, header2: value2" + pub static ref AI_HTTP_HEADERS: Vec<(String, String)> = { + std::env::var("AI_HTTP_HEADERS") + .ok() + .map(|headers_str| { + headers_str + .split(',') + .filter_map(|header| { + let parts: Vec<&str> = header.splitn(2, ':').collect(); + if parts.len() == 2 { + let name = parts[0].trim().to_string(); + let value = parts[1].trim().to_string(); + if !name.is_empty() && !value.is_empty() { + Some((name, value)) + } else { + None + } + } else { + None + } + }) + .collect() + }) + .unwrap_or_default() + }; +} + +/// AWS Bedrock do not handle structured output query param, so we use a tool for structured output. Same for every Claude models. +pub fn should_use_structured_output_tool(provider: &AIProvider, model: &str) -> bool { + model.contains("claude") || provider == &AIProvider::AWSBedrock +} + +/// Extract text content from OpenAIContent, joining parts with space if multiple +pub fn extract_text_content(content: &OpenAIContent) -> String { + match content { + OpenAIContent::Text(text) => text.clone(), + OpenAIContent::Parts(parts) => parts + .iter() + .filter_map(|p| { + if let ContentPart::Text { text } = p { + Some(text.as_str()) + } else { + None + } + }) + .collect::>() + .join(""), + } +} diff --git a/backend/windmill-api-auth/src/auth.rs b/backend/windmill-api-auth/src/auth.rs index d4c448e934..ac4017b81c 100644 --- a/backend/windmill-api-auth/src/auth.rs +++ b/backend/windmill-api-auth/src/auth.rs @@ -715,7 +715,8 @@ pub async fn resolve_opt_job_authed( fn username_override_from_label(label: Option) -> Option { match label { Some(label) - if label.starts_with("webhook-") + if label.starts_with("ephemeral-webhook-") + || label.starts_with("webhook-") || label.starts_with("http-") || label.starts_with("email-") || label.starts_with("ws-") => diff --git a/backend/windmill-api-inputs/src/lib.rs b/backend/windmill-api-inputs/src/lib.rs index 154e02e5a9..348b434eea 100644 --- a/backend/windmill-api-inputs/src/lib.rs +++ b/backend/windmill-api-inputs/src/lib.rs @@ -172,6 +172,7 @@ async fn get_input_history( kind IN ('preview', 'flowpreview') as is_preview \ FROM v2_job JOIN v2_job_completed USING (id) \ WHERE v2_job.workspace_id = $3 AND {} = $1 AND kind = any($2) \ + AND v2_job.script_entrypoint_override IS NULL \ {args_query} AND v2_job_completed.status != 'skipped' {include_non_root} \ ORDER BY v2_job.created_at DESC LIMIT $4\ ) t ORDER BY completed_at DESC LIMIT $5 OFFSET $6", diff --git a/backend/windmill-api-integration-tests/tests/token_hash.rs b/backend/windmill-api-integration-tests/tests/token_hash.rs index 6f4ba6b0ac..45d324a3d4 100644 --- a/backend/windmill-api-integration-tests/tests/token_hash.rs +++ b/backend/windmill-api-integration-tests/tests/token_hash.rs @@ -274,7 +274,9 @@ async fn test_plaintext_backward_compat(db: Pool) -> anyhow::Result<() ); // --- Phase 2: All workers upgraded (version >= 1.650.0) --- - MIN_VERSION.store(std::sync::Arc::new(MIN_VERSION_SUPPORTS_TOKEN_HASH.version().clone())); + MIN_VERSION.store(std::sync::Arc::new( + MIN_VERSION_SUPPORTS_TOKEN_HASH.version().clone(), + )); let resp = authed(client().post(format!("{base}/tokens/create"))) .json(&json!({"label": "new-worker-token"})) @@ -324,7 +326,7 @@ async fn test_plaintext_backward_compat(db: Pool) -> anyhow::Result<() async fn test_rotate_webhook_token(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; - use windmill_native_triggers::{delete_token_by_hash, rotate_webhook_token}; + use windmill_native_triggers::{delete_token_by_hash, rotate_webhook_token, ServiceName}; // Insert a token directly with known values let original_token = "test-webhook-token-original-1234"; @@ -342,7 +344,7 @@ async fn test_rotate_webhook_token(db: Pool) -> anyhow::Result<()> { .await?; // Rotate the token - let rotated = rotate_webhook_token(&db, &original_hash) + let rotated = rotate_webhook_token(&db, &original_hash, ServiceName::Google) .await? .expect("rotate must return Some for existing token"); @@ -350,16 +352,27 @@ async fn test_rotate_webhook_token(db: Pool) -> anyhow::Result<()> { assert_ne!(rotated.new_token, original_token); assert_eq!(rotated.old_token_hash, original_hash); - // New token's hash should exist in DB + // New token's hash should exist in DB with the per-service label and expiration let new_hash = hash_token(&rotated.new_token); - let exists: bool = sqlx::query_scalar!( - "SELECT EXISTS(SELECT 1 FROM token WHERE token_hash = $1) AS exists", + let new_row = sqlx::query!( + "SELECT label, expiration FROM token WHERE token_hash = $1", new_hash ) - .fetch_one(&db) + .fetch_optional(&db) .await? - .unwrap_or(false); - assert!(exists, "new token hash must exist in DB after rotation"); + .expect("new token hash must exist in DB after rotation"); + assert!( + new_row + .label + .as_deref() + .is_some_and(|l| l.starts_with("ephemeral-webhook-google-")), + "rotated token must carry an ephemeral-webhook-google-* label, got {:?}", + new_row.label + ); + assert!( + new_row.expiration.is_some(), + "rotated Google token must carry an expiration" + ); // Old token should still exist (deletion deferred to caller) let old_exists: bool = sqlx::query_scalar!( @@ -389,7 +402,7 @@ async fn test_rotate_webhook_token(db: Pool) -> anyhow::Result<()> { assert!(!old_gone, "old token must be gone after explicit deletion"); // Rotating a non-existent hash should return None - let result = rotate_webhook_token(&db, "nonexistent_hash").await?; + let result = rotate_webhook_token(&db, "nonexistent_hash", ServiceName::Google).await?; assert!( result.is_none(), "rotating a non-existent token must return None" diff --git a/backend/windmill-api-jobs/src/concurrency_groups.rs b/backend/windmill-api-jobs/src/concurrency_groups.rs index f3ef3b14a8..d8e7a54112 100644 --- a/backend/windmill-api-jobs/src/concurrency_groups.rs +++ b/backend/windmill-api-jobs/src/concurrency_groups.rs @@ -226,6 +226,7 @@ async fn get_concurrent_intervals( trigger_kind: _, include_args: _, broad_filter: _, + excludes_entrypoint_override: _, } => true, _ => false, }; diff --git a/backend/windmill-api-jobs/src/execution.rs b/backend/windmill-api-jobs/src/execution.rs index 555d7a4d8b..e4359e19da 100644 --- a/backend/windmill-api-jobs/src/execution.rs +++ b/backend/windmill-api-jobs/src/execution.rs @@ -248,8 +248,11 @@ lazy_static::lazy_static! { #[derive(Deserialize)] pub struct WindmillCompositeResult { + #[serde(alias = "wm_status_code")] windmill_status_code: Option, + #[serde(alias = "wm_content_type")] windmill_content_type: Option, + #[serde(alias = "wm_headers")] windmill_headers: Option>, result: Option>, } diff --git a/backend/windmill-api-jobs/src/query.rs b/backend/windmill-api-jobs/src/query.rs index 38e139b77b..e6c57e6c5b 100644 --- a/backend/windmill-api-jobs/src/query.rs +++ b/backend/windmill-api-jobs/src/query.rs @@ -216,6 +216,10 @@ pub fn filter_list_queue_query( sqlb.and_where("trigger_kind IS DISTINCT FROM 'schedule'"); } + if lq.excludes_entrypoint_override.unwrap_or(false) { + sqlb.and_where_is_null("v2_job.script_entrypoint_override"); + } + if let Some(tk) = &lq.trigger_kind { let quoted: Vec<_> = tk.values.iter().map(|v| quote(&format!("{}", v))).collect(); if tk.negated { @@ -519,6 +523,10 @@ pub fn filter_list_completed_query( sqlb.and_where("trigger_kind IS DISTINCT FROM 'schedule'"); } + if lq.excludes_entrypoint_override.unwrap_or(false) { + sqlb.and_where_is_null("v2_job.script_entrypoint_override"); + } + if let Some(tk) = &lq.trigger_kind { let quoted: Vec<_> = tk.values.iter().map(|v| quote(&format!("{}", v))).collect(); if tk.negated { @@ -624,6 +632,7 @@ mod tests { trigger_path: None, include_args: None, broad_filter: None, + excludes_entrypoint_override: None, } } @@ -668,6 +677,7 @@ mod tests { trigger_path: None, include_args: None, broad_filter: None, + excludes_entrypoint_override: None, } } diff --git a/backend/windmill-api-jobs/src/types.rs b/backend/windmill-api-jobs/src/types.rs index e07328e2be..bc03a04289 100644 --- a/backend/windmill-api-jobs/src/types.rs +++ b/backend/windmill-api-jobs/src/types.rs @@ -122,6 +122,7 @@ pub struct ListQueueQuery { pub trigger_path: Option>, pub include_args: Option, pub broad_filter: Option, + pub excludes_entrypoint_override: Option, } #[derive(Deserialize, Clone)] @@ -167,6 +168,7 @@ pub struct ListCompletedQuery { pub trigger_path: Option>, pub include_args: Option, pub broad_filter: Option, + pub excludes_entrypoint_override: Option, } impl From for ListQueueQuery { @@ -202,6 +204,7 @@ impl From for ListQueueQuery { trigger_path: lcq.trigger_path, include_args: lcq.include_args, broad_filter: lcq.broad_filter, + excludes_entrypoint_override: lcq.excludes_entrypoint_override, } } } @@ -704,6 +707,7 @@ mod tests { trigger_path: None, include_args: None, broad_filter: None, + excludes_entrypoint_override: None, }; let lqq: ListQueueQuery = lcq.into(); @@ -772,6 +776,7 @@ mod tests { trigger_path: None, include_args: None, broad_filter: None, + excludes_entrypoint_override: None, }; let lqq: ListQueueQuery = lcq.into(); diff --git a/backend/windmill-api-settings/src/ee_oss.rs b/backend/windmill-api-settings/src/ee_oss.rs index d0e0f69b62..92b3d6ad53 100644 --- a/backend/windmill-api-settings/src/ee_oss.rs +++ b/backend/windmill-api-settings/src/ee_oss.rs @@ -8,7 +8,11 @@ use anyhow::anyhow; pub async fn validate_license_key( _license_key: String, _db: Option<&windmill_common::DB>, -) -> anyhow::Result<(String, bool)> { +) -> anyhow::Result<( + String, + bool, + Option, +)> { // Implementation is not open source Err(anyhow!("License can't be validated in Windmill CE")) } diff --git a/backend/windmill-api-settings/src/lib.rs b/backend/windmill-api-settings/src/lib.rs index f027046808..09b52a3d22 100644 --- a/backend/windmill-api-settings/src/lib.rs +++ b/backend/windmill-api-settings/src/lib.rs @@ -37,13 +37,13 @@ use axum::{ use serde_json::json; use serde::{Deserialize, Serialize}; +use windmill_ai::ai_cache::bump_instance_ai_config_revision; #[cfg(feature = "enterprise")] use windmill_common::ee_oss::{send_critical_alert, CriticalAlertKind, CriticalErrorChannel}; #[cfg(all(feature = "private", feature = "enterprise"))] use windmill_common::secret_backend::{ AwsSecretsManagerSettings, AzureKeyVaultSettings, SecretMigrationReport, VaultSettings, }; -use windmill_ai::ai_cache::bump_instance_ai_config_revision; use windmill_common::{ email_oss::send_email_plain_text, error::{self, JsonResult, Result}, @@ -118,6 +118,8 @@ pub fn global_service() -> Router { get(get_latest_key_renewal_attempt), ) .route("/renew_license_key", post(renew_license_key)) + .route("/offline_license_status", get(get_offline_license_status)) + .route("/instance_hash", get(get_instance_hash)) .route("/customer_portal", post(create_customer_portal_session)) .route("/test_critical_channels", post(test_critical_channels)) .route("/critical_alerts", get(get_critical_alerts)) @@ -340,7 +342,7 @@ pub async fn test_license_key( Json(TestKey { license_key }): Json, ) -> error::Result { require_super_admin(&db, &authed.email).await?; - let (_, expired) = validate_license_key(license_key, Some(&db)).await?; + let (_, expired, _offline_meta) = validate_license_key(license_key, Some(&db)).await?; if expired { Err(error::Error::BadRequest("Expired license key".to_string())) @@ -349,6 +351,53 @@ pub async fn test_license_key( } } +#[derive(serde::Serialize)] +pub struct InstanceHash { + pub instance_hash: Option, +} + +/// Returns the live cap status for an offline license, or `null` when no +/// offline license is loaded. Used by the superadmin settings panel. +pub async fn get_offline_license_status( + Extension(db): Extension, + authed: ApiAuthed, +) -> error::JsonResult> { + require_super_admin(&db, &authed.email).await?; + + let offline = (**windmill_common::ee_oss::LICENSE_OFFLINE_METADATA.load()).clone(); + let is_offline = matches!(&offline, Some(m) if m.is_offline()); + + if !is_offline { + return Ok(Json(None)); + } + + #[cfg(feature = "enterprise")] + let cap = windmill_common::ee_oss::enforce_offline_caps(&db) + .await + .map_err(|e| error::Error::internal_err(format!("enforce_offline_caps: {e:#}")))?; + #[cfg(not(feature = "enterprise"))] + let cap: Option = None; + + Ok(Json(cap)) +} + +/// Returns the per-instance binding hash that goes into offline license keys. +/// Admin invokes via `curl` with their personal token when requesting a key +/// from support. +pub async fn get_instance_hash( + Extension(db): Extension, + authed: ApiAuthed, +) -> error::JsonResult { + require_super_admin(&db, &authed.email).await?; + #[cfg(feature = "enterprise")] + let hash = windmill_common::ee_oss::compute_instance_hash(&db) + .await + .map_err(|e| error::Error::internal_err(format!("compute_instance_hash: {e:#}")))?; + #[cfg(not(feature = "enterprise"))] + let hash: Option = None; + Ok(Json(InstanceHash { instance_hash: hash })) +} + pub async fn get_local_settings( Extension(db): Extension, authed: ApiAuthed, diff --git a/backend/windmill-api-workspaces/src/workspaces.rs b/backend/windmill-api-workspaces/src/workspaces.rs index f479b8c719..95c7157d61 100644 --- a/backend/windmill-api-workspaces/src/workspaces.rs +++ b/backend/windmill-api-workspaces/src/workspaces.rs @@ -5233,6 +5233,13 @@ async fn invite_user( nu.email = nu.email.to_lowercase(); + #[cfg(feature = "enterprise")] + if let Some(msg) = + windmill_common::ee_oss::check_seat_cap_for_new_user(&db, &nu.email, nu.operator).await? + { + return Err(Error::BadRequest(msg)); + } + let mut tx = db.begin().await?; let already_in_workspace = sqlx::query_scalar!( @@ -5306,6 +5313,13 @@ async fn add_user( nu.email = nu.email.to_lowercase(); + #[cfg(feature = "enterprise")] + if let Some(msg) = + windmill_common::ee_oss::check_seat_cap_for_new_user(&db, &nu.email, nu.operator).await? + { + return Err(Error::BadRequest(msg)); + } + let mut tx = db.begin().await?; let already_exists_email = sqlx::query_scalar!( @@ -7351,6 +7365,7 @@ async fn list_ws_specific( WHERE v.workspace_id = s.workspace_id AND v.path = s.path )) ) + ORDER BY s.item_kind, s.path "#, &w_id ) diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index dd3c480241..258e268f88 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.697.0 + version: 1.700.2 title: Windmill API contact: @@ -1779,6 +1779,63 @@ paths: schema: type: string + /settings/offline_license_status: + get: + summary: get cap-usage status for the currently-loaded offline license + description: | + Returns the live cap status (seats used vs cap, current CU vs cap) for + the offline license key currently in use. Returns `null` if no offline + license is loaded. Super-admin only. + operationId: getOfflineLicenseStatus + tags: + - setting + responses: + "200": + description: cap status (or null when no offline license) + content: + application/json: + schema: + type: object + nullable: true + properties: + seats_used: + type: number + description: Author-equivalent seats consumed (authors + 0.5 × operators) + seats_cap: + type: integer + author_count: + type: integer + operator_count: + type: integer + current_cu: + type: number + description: Sum of CU rate across workers that pinged in the last 2 minutes. + cu_cap: + type: number + cu_over_cap: + type: boolean + + /settings/instance_hash: + get: + summary: per-instance binding hash for offline license issuance + description: | + Returns the hash a superadmin shares with Windmill support when + requesting an offline license. Super-admin only. + operationId: getInstanceHash + tags: + - setting + responses: + '200': + description: instance hash + content: + application/json: + schema: + type: object + properties: + instance_hash: + type: string + nullable: true + /settings/customer_portal: post: summary: create customer portal session @@ -12000,6 +12057,11 @@ paths: in: query schema: type: boolean + - name: excludes_entrypoint_override + description: exclude jobs that were started with a `_ENTRYPOINT_OVERRIDE` arg (e.g. dynamic-select helper runs and preprocessor previews) + in: query + schema: + type: boolean - name: broad_filter description: broad search across multiple fields (case-insensitive substring match on path, tag, schedule path, trigger kind, label) in: query @@ -20958,6 +21020,9 @@ components: jwt_role: type: string description: Vault JWT auth role name for Windmill (optional, if not provided token auth is used) + jwt_mount_path: + type: string + description: Mount path for the JWT auth method in Vault (optional, defaults to "jwt"). Set this when the JWT auth method is mounted at a non-default path, e.g. via `vault auth enable -path= jwt`. namespace: type: string description: Vault Enterprise namespace (optional) diff --git a/backend/windmill-api/src/ai.rs b/backend/windmill-api/src/ai.rs index a2c4ceb811..8b415997c2 100644 --- a/backend/windmill-api/src/ai.rs +++ b/backend/windmill-api/src/ai.rs @@ -15,11 +15,12 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, value::RawValue}; use std::collections::HashMap; use std::time::Duration; -use windmill_audit::{audit_oss::audit_log, ActionKind}; use windmill_ai::ai_cache::current_instance_ai_config_revision; use windmill_ai::ai_providers::{ empty_string_as_none, AIPlatform, AIProvider, ProviderConfig, ProviderModel, }; +use windmill_ai::utils::AI_HTTP_HEADERS; +use windmill_audit::{audit_oss::audit_log, ActionKind}; use windmill_common::db::UserDB; use windmill_common::error::{to_anyhow, Error, Result}; use windmill_common::utils::configure_client; @@ -101,32 +102,6 @@ lazy_static::lazy_static! { pub static ref AI_REQUEST_CACHE: Cache<(String, AIProvider), ExpiringAIRequestConfig> = Cache::new(500); - /// Parse AI_HTTP_HEADERS environment variable into a vector of (header_name, header_value) tuples - /// Format: "header1: value1, header2: value2" - static ref AI_HTTP_HEADERS: Vec<(String, String)> = { - std::env::var("AI_HTTP_HEADERS") - .ok() - .map(|headers_str| { - headers_str - .split(',') - .filter_map(|header| { - let parts: Vec<&str> = header.splitn(2, ':').collect(); - if parts.len() == 2 { - let name = parts[0].trim().to_string(); - let value = parts[1].trim().to_string(); - if !name.is_empty() && !value.is_empty() { - Some((name, value)) - } else { - None - } - } else { - None - } - }) - .collect() - }) - .unwrap_or_default() - }; } pub(crate) fn invalidate_ai_request_cache_for_workspace(workspace_id: &str) { diff --git a/backend/windmill-api/src/ee_oss.rs b/backend/windmill-api/src/ee_oss.rs index a1f7a54d20..e5a7f71cbc 100644 --- a/backend/windmill-api/src/ee_oss.rs +++ b/backend/windmill-api/src/ee_oss.rs @@ -10,7 +10,11 @@ use anyhow::anyhow; pub async fn validate_license_key( _license_key: String, _db: Option<&crate::db::DB>, -) -> anyhow::Result<(String, bool)> { +) -> anyhow::Result<( + String, + bool, + Option, +)> { // Implementation is not open source Err(anyhow!("License can't be validated in Windmill CE")) } diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index efe03f73dd..ff72ddce70 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -1067,10 +1067,15 @@ impl<'a> GetQuery<'a> { .ok() .inspect(|data| job.raw_flow = Some(sqlx::types::Json(data.raw_flow.clone()))); } - if self.with_code && job.job_kind() == &JobKind::Preview { + if self.with_code + && matches!( + job.job_kind(), + JobKind::Preview | JobKind::FlowScript | JobKind::AppScript + ) + { // Try to fetch the code from the cache, fallback to the preview code. - // NOTE: This could check for the job kinds instead of the `or_else` but it's not - // necessary as `fetch_script` return early if the job kind is not a preview one. + // `fetch_script` resolves FlowScript / AppScript via their runnable_id; for + // Preview jobs it returns early and we fall through to `fetch_preview_script`. let conn = Connection::from(db.clone()); cache::job::fetch_script(db.clone(), job.job_kind(), hash) .or_else(|_| cache::job::fetch_preview_script(&conn, &id, raw_lock, raw_code)) diff --git a/backend/windmill-common/src/ee_oss.rs b/backend/windmill-common/src/ee_oss.rs index 15d9e7cb5e..e68bc00c6e 100644 --- a/backend/windmill-common/src/ee_oss.rs +++ b/backend/windmill-common/src/ee_oss.rs @@ -18,6 +18,60 @@ lazy_static::lazy_static! { pub static ref LICENSE_KEY_VALID: AtomicBool = AtomicBool::new(true); pub static ref LICENSE_KEY_ID: arc_swap::ArcSwap = arc_swap::ArcSwap::from_pointee("".to_string()); pub static ref LICENSE_KEY: arc_swap::ArcSwap = arc_swap::ArcSwap::from_pointee("".to_string()); + pub static ref LICENSE_OFFLINE_METADATA: arc_swap::ArcSwap> = arc_swap::ArcSwap::from_pointee(None); + pub static ref LICENSE_OFFLINE_OVER_CU_CAP: AtomicBool = AtomicBool::new(false); + pub static ref LICENSE_OFFLINE_LAST_STATUS: arc_swap::ArcSwap> = arc_swap::ArcSwap::from_pointee(None); + pub static ref LICENSE_OFFLINE_LAST_CHECKED_AT: arc_swap::ArcSwap>> = arc_swap::ArcSwap::from_pointee(None); +} + +#[cfg(not(feature = "private"))] +#[derive(Clone, Debug, Deserialize, serde::Serialize)] +pub struct OfflineMetadata { + pub v: u32, + pub kind: String, + pub hash: String, + pub seats: i64, + pub cu_limit: f64, +} + +#[cfg(not(feature = "private"))] +impl OfflineMetadata { + pub fn is_offline(&self) -> bool { + self.kind == "offline" + } +} + +#[cfg(not(feature = "private"))] +#[derive(Clone, Debug, serde::Serialize)] +pub struct OfflineCapStatus { + pub seats_used: f64, + pub seats_cap: i64, + pub author_count: i64, + pub operator_count: i64, + pub current_cu: f64, + pub cu_cap: f64, + pub cu_over_cap: bool, +} + +#[cfg(all(feature = "enterprise", not(feature = "private")))] +pub async fn check_seat_cap_for_new_user( + _db: &DB, + _email: &str, + _new_user_is_operator: bool, +) -> anyhow::Result> { + Ok(None) +} + +#[cfg(all(feature = "enterprise", not(feature = "private")))] +pub async fn compute_instance_hash(_db: &DB) -> anyhow::Result> { + // Implementation is not open source + Ok(None) +} + +#[cfg(all(feature = "enterprise", not(feature = "private")))] +pub async fn enforce_offline_caps(_db: &DB) -> anyhow::Result> { + // Implementation is not open source + Ok(None) } #[cfg(not(feature = "private"))] diff --git a/backend/windmill-common/src/instance_config.rs b/backend/windmill-common/src/instance_config.rs index e9a9731f74..e19182be30 100644 --- a/backend/windmill-common/src/instance_config.rs +++ b/backend/windmill-common/src/instance_config.rs @@ -200,7 +200,15 @@ fn opaque_json_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schem /// Typed global settings with schema validation. /// Known settings have explicit fields; unknown settings pass through via `extra`. +/// +/// `#[serde(remote = "Self")]` turns the derived (de)serializers into inherent +/// associated functions so we can wrap them with the manual `Deserialize` impl +/// below. The wrapper preserves explicit `null` values (which the typed +/// `Option` fields would otherwise silently drop on round-trip through +/// `to_settings_map`) by stashing them in `extra`, where they survive +/// re-serialization and reach `diff_global_settings` as proper deletes. #[derive(Deserialize, Serialize, Clone, Debug, Default)] +#[serde(remote = "Self")] #[cfg_attr(feature = "instance_config_schema", derive(schemars::JsonSchema))] pub struct GlobalSettings { // Numeric settings @@ -373,6 +381,42 @@ pub struct GlobalSettings { pub extra: BTreeMap, } +impl Serialize for GlobalSettings { + fn serialize(&self, serializer: S) -> Result { + Self::serialize(self, serializer) + } +} + +impl<'de> Deserialize<'de> for GlobalSettings { + fn deserialize>(deserializer: D) -> Result { + // Capture top-level keys explicitly set to `null` so they survive the + // `to_settings_map` round-trip — typed `Option` fields all use + // `skip_serializing_if = "Option::is_none"`, which would otherwise + // silently drop the null. We stash the null entries in `extra`, which + // serializes them back out as `null` for `diff_global_settings` to + // route to deletes. + let mut value = serde_json::Value::deserialize(deserializer)?; + let null_keys: Vec = value + .as_object() + .map(|m| { + m.iter() + .filter_map(|(k, v)| v.is_null().then(|| k.clone())) + .collect() + }) + .unwrap_or_default(); + if let Some(obj) = value.as_object_mut() { + for k in &null_keys { + obj.remove(k); + } + } + let mut s = Self::deserialize(value).map_err(serde::de::Error::custom)?; + for k in null_keys { + s.extra.insert(k, serde_json::Value::Null); + } + Ok(s) + } +} + impl GlobalSettings { /// Convert to a flat `BTreeMap` suitable for DB sync. pub fn to_settings_map(&self) -> BTreeMap { @@ -1930,6 +1974,101 @@ mod tests { // to_settings_map edge cases // ----------------------------------------------------------------------- + /// Regression test for bulk-endpoint deletion of typed settings. + /// + /// A naive `#[derive(Deserialize)]` would route + /// `{"object_store_cache_config": null}` to the typed `Option<...>` field + /// as `None`, and `skip_serializing_if = "Option::is_none"` would then + /// strip it from `to_settings_map`, so `diff_global_settings` (Merge mode) + /// would never see the deletion. The manual `Deserialize` impl on + /// `GlobalSettings` captures explicit top-level nulls into `extra` so they + /// survive the round-trip and reach `diff_global_settings` as proper + /// deletes. + #[test] + fn explicit_null_on_typed_field_survives_round_trip() { + let json = serde_json::json!({ + "object_store_cache_config": null, + "secret_backend": null, + "smtp_settings": null, + "base_url": "https://x", + }); + let settings: GlobalSettings = + serde_json::from_value(json).expect("null should deserialize"); + assert!(settings.object_store_cache_config.is_none()); + assert!(settings.secret_backend.is_none()); + assert!(settings.smtp_settings.is_none()); + assert_eq!(settings.base_url.as_deref(), Some("https://x")); + let map = settings.to_settings_map(); + assert_eq!(map["object_store_cache_config"], serde_json::Value::Null); + assert_eq!(map["secret_backend"], serde_json::Value::Null); + assert_eq!(map["smtp_settings"], serde_json::Value::Null); + assert_eq!(map["base_url"], serde_json::json!("https://x")); + } + + /// Absent keys remain absent — critical so a PUT that only sets a single + /// field doesn't accidentally delete every other setting in Merge mode. + #[test] + fn absent_typed_fields_do_not_appear_in_map() { + let settings: GlobalSettings = + serde_json::from_value(serde_json::json!({"base_url": "https://x"})).unwrap(); + let map = settings.to_settings_map(); + assert!(!map.contains_key("object_store_cache_config")); + assert!(!map.contains_key("smtp_settings")); + assert_eq!(map.get("base_url"), Some(&serde_json::json!("https://x"))); + } + + /// End-to-end: deserialize → `to_settings_map` → diff in Merge mode with + /// an explicit null produces a delete (the scenario that silently failed + /// before the manual `Deserialize` impl). + #[test] + fn deserialize_then_diff_deletes_typed_null() { + let mut current = BTreeMap::new(); + current.insert( + "object_store_cache_config".to_string(), + serde_json::json!({"type": "S3", "bucket": "b"}), + ); + let desired: GlobalSettings = + serde_json::from_value(serde_json::json!({"object_store_cache_config": null})).unwrap(); + let desired_map = desired.to_settings_map(); + let diff = diff_global_settings(¤t, &desired_map, ApplyMode::Merge); + assert!(diff.upserts.is_empty()); + assert_eq!(diff.deletes, vec!["object_store_cache_config".to_string()]); + } + + /// Nested nulls inside a typed sub-struct are not promoted to top-level + /// deletes — only the top-level key matters, mirroring the per-key API. + #[test] + fn nested_null_inside_typed_field_is_not_treated_as_top_level_null() { + let settings: GlobalSettings = + serde_json::from_value(serde_json::json!({"smtp_settings": {"smtp_host": null}})) + .unwrap(); + let map = settings.to_settings_map(); + assert!( + map["smtp_settings"].is_object(), + "smtp_settings should be an object, not null" + ); + } + + /// Unknown/extra keys with explicit null still flow through `extra` — this + /// was already correct before the manual impl; guard against regression. + #[test] + fn explicit_null_on_extra_field_survives_round_trip() { + let settings: GlobalSettings = + serde_json::from_value(serde_json::json!({"unknown_legacy_setting": null})).unwrap(); + let map = settings.to_settings_map(); + assert_eq!(map["unknown_legacy_setting"], serde_json::Value::Null); + } + + /// Top-level non-object input must reject with a deserialize error rather + /// than silently producing defaults. Matches the previous derive behavior. + #[test] + fn non_object_top_level_input_errors() { + assert!(serde_json::from_value::(serde_json::json!(null)).is_err()); + assert!(serde_json::from_value::(serde_json::json!("s")).is_err()); + assert!(serde_json::from_value::(serde_json::json!(42)).is_err()); + assert!(serde_json::from_value::(serde_json::json!([])).is_err()); + } + #[test] fn to_settings_map_empty_defaults() { let settings = GlobalSettings::default(); diff --git a/backend/windmill-common/src/lib.rs b/backend/windmill-common/src/lib.rs index 7c7e3f79ed..39884e3d35 100644 --- a/backend/windmill-common/src/lib.rs +++ b/backend/windmill-common/src/lib.rs @@ -121,6 +121,36 @@ pub const PRIVATE_HUB_MIN_VERSION: i32 = 10_000_000; pub const SERVICE_LOG_RETENTION_SECS: i64 = 60 * 60 * 24 * 14; // 2 weeks retention period for logs pub const WM_DEPLOYERS_GROUP: &str = "wm_deployers"; +/// Canonical form of a base URL, used as one of the inputs to the offline-license +/// instance hash (`compute_instance_hash`). +/// +/// Rules: lowercase scheme and host, drop default ports (80/443), strip path/query/fragment, +/// strip trailing slash. If URL parsing fails, falls back to a best-effort lowercase + +/// trailing-slash strip so two semantically-equivalent inputs still produce the same +/// canonical form. +pub fn canonical_base_url(input: &str) -> String { + let trimmed = input.trim(); + if trimmed.is_empty() { + return String::new(); + } + match url::Url::parse(trimmed) { + Ok(u) => { + let scheme = u.scheme().to_ascii_lowercase(); + let host = u + .host_str() + .map(|h| h.to_ascii_lowercase()) + .unwrap_or_default(); + let port = match (u.port(), scheme.as_str()) { + (Some(80), "http") | (Some(443), "https") => String::new(), + (Some(p), _) => format!(":{p}"), + (None, _) => String::new(), + }; + format!("{scheme}://{host}{port}") + } + Err(_) => trimmed.trim_end_matches('/').to_ascii_lowercase(), + } +} + /// Checks if the user is allowed to preserve on_behalf_of values (admin or deployer). pub fn can_preserve_on_behalf_of(authed: &impl db::Authable) -> bool { authed.is_admin() || authed.groups().iter().any(|g| g == &WM_DEPLOYERS_GROUP) diff --git a/backend/windmill-common/src/secret_backend/mod.rs b/backend/windmill-common/src/secret_backend/mod.rs index a75f51ea70..36f35f0cf8 100644 --- a/backend/windmill-common/src/secret_backend/mod.rs +++ b/backend/windmill-common/src/secret_backend/mod.rs @@ -122,6 +122,11 @@ pub struct VaultSettings { /// Optional - if not provided, token auth is used #[serde(skip_serializing_if = "Option::is_none")] pub jwt_role: Option, + /// Mount path for the JWT auth method in Vault (defaults to "jwt"). + /// Set this when the JWT auth method is mounted at a non-default path, + /// e.g. via `vault auth enable -path=my-mount jwt`. + #[serde(skip_serializing_if = "Option::is_none")] + pub jwt_mount_path: Option, /// Vault Enterprise namespace (optional) #[serde(skip_serializing_if = "Option::is_none")] pub namespace: Option, diff --git a/backend/windmill-common/src/secret_backend/tests.rs b/backend/windmill-common/src/secret_backend/tests.rs index 630b6cf023..3e2a12382a 100644 --- a/backend/windmill-common/src/secret_backend/tests.rs +++ b/backend/windmill-common/src/secret_backend/tests.rs @@ -26,6 +26,7 @@ mod tests { address: "http://127.0.0.1:8200".to_string(), mount_path: "windmill".to_string(), jwt_role: Some("windmill-secrets".to_string()), + jwt_mount_path: None, namespace: None, token: Some("test-root-token".to_string()), skip_ssl_verify: None, diff --git a/backend/windmill-common/src/utils.rs b/backend/windmill-common/src/utils.rs index efe5481e1d..17d9cfd804 100644 --- a/backend/windmill-common/src/utils.rs +++ b/backend/windmill-common/src/utils.rs @@ -447,7 +447,9 @@ pub async fn get_license_id_or_uid<'c, E: sqlx::Executor<'c, Database = Postgres } } -async fn get_instance_uid<'c, E: sqlx::Executor<'c, Database = Postgres>>(db: E) -> Result { +pub async fn get_instance_uid<'c, E: sqlx::Executor<'c, Database = Postgres>>( + db: E, +) -> Result { let uid_value = sqlx::query_scalar!( "SELECT value FROM global_settings WHERE name = $1", UNIQUE_ID_SETTING diff --git a/backend/windmill-common/tests/secret_backend_integration.rs b/backend/windmill-common/tests/secret_backend_integration.rs index 070c70defa..350fc297af 100644 --- a/backend/windmill-common/tests/secret_backend_integration.rs +++ b/backend/windmill-common/tests/secret_backend_integration.rs @@ -91,6 +91,7 @@ mod tests { .unwrap_or_else(|_| "http://127.0.0.1:8200".to_string()), mount_path: "windmill".to_string(), jwt_role: None, // Static token mode + jwt_mount_path: None, namespace: None, token: Some( std::env::var("VAULT_TOKEN").unwrap_or_else(|_| "test-root-token".to_string()), @@ -106,6 +107,7 @@ mod tests { .unwrap_or_else(|_| "http://127.0.0.1:8200".to_string()), mount_path: "windmill".to_string(), jwt_role: Some("windmill-secrets".to_string()), // JWT mode + jwt_mount_path: None, namespace: None, token: None, // No static token - use JWT skip_ssl_verify: None, @@ -203,7 +205,10 @@ mod tests { println!("Testing Vault connection with JWT auth..."); println!(" Address: {}", settings.address); println!(" JWT Role: {:?}", settings.jwt_role); - println!(" BASE_URL: {}", (**windmill_common::BASE_URL.load()).clone()); + println!( + " BASE_URL: {}", + (**windmill_common::BASE_URL.load()).clone() + ); let result = test_vault_connection(&settings, Some(&db)).await; assert!( @@ -274,13 +279,15 @@ mod tests { // Encrypt fixture placeholders with real workspace keys encrypt_fixture_secrets(&db).await; - let secret_count = sqlx::query_scalar!( - "SELECT COUNT(*) FROM variable WHERE is_secret = true" - ) - .fetch_one(&db) - .await - .expect("Failed to count secrets"); - println!("Found {} secrets in database before migration", secret_count.unwrap_or(0)); + let secret_count = + sqlx::query_scalar!("SELECT COUNT(*) FROM variable WHERE is_secret = true") + .fetch_one(&db) + .await + .expect("Failed to count secrets"); + println!( + "Found {} secrets in database before migration", + secret_count.unwrap_or(0) + ); // Run migration println!("Migrating secrets to Vault..."); @@ -288,8 +295,10 @@ mod tests { .await .expect("Migration to Vault failed"); - println!("Migration report: total={}, migrated={}, failed={}", - report.total_secrets, report.migrated_count, report.failed_count); + println!( + "Migration report: total={}, migrated={}, failed={}", + report.total_secrets, report.migrated_count, report.failed_count + ); if !report.failures.is_empty() { for f in &report.failures { @@ -307,7 +316,11 @@ mod tests { .get_secret(ws, path) .await .unwrap_or_else(|e| panic!("Failed to read {}/{} from Vault: {:?}", ws, path, e)); - assert_eq!(value, expected_plaintext, "Vault value mismatch for {}/{}", ws, path); + assert_eq!( + value, expected_plaintext, + "Vault value mismatch for {}/{}", + ws, path + ); println!(" ✓ {}/{} correct in Vault", ws, path); } @@ -346,8 +359,10 @@ mod tests { .await .expect("Migration to database failed"); - println!("Migration report: total={}, migrated={}, failed={}", - report.total_secrets, report.migrated_count, report.failed_count); + println!( + "Migration report: total={}, migrated={}, failed={}", + report.total_secrets, report.migrated_count, report.failed_count + ); assert_eq!(report.failed_count, 0, "Migration had failures"); assert!(report.migrated_count > 0, "No secrets were migrated"); @@ -364,7 +379,11 @@ mod tests { let mc = build_crypt(&db, ws).await.unwrap(); let decrypted = decrypt(&mc, row).expect("Failed to decrypt restored value"); - assert_eq!(decrypted, expected_plaintext, "Restored value mismatch for {}/{}", ws, path); + assert_eq!( + decrypted, expected_plaintext, + "Restored value mismatch for {}/{}", + ws, path + ); println!(" ✓ {}/{} correctly restored in DB", ws, path); } @@ -488,11 +507,19 @@ mod tests { .await .unwrap_or_else(|_| panic!("Secret {}/{} not found after round-trip", ws, path)); - assert_ne!(encrypted, "ROUND_TRIP_CLEARED", "Secret {}/{} was not restored", ws, path); + assert_ne!( + encrypted, "ROUND_TRIP_CLEARED", + "Secret {}/{} was not restored", + ws, path + ); let mc = build_crypt(&db, ws).await.unwrap(); let decrypted = decrypt(&mc, encrypted).expect("Failed to decrypt"); - assert_eq!(decrypted, expected_plaintext, "Round-trip value mismatch for {}/{}", ws, path); + assert_eq!( + decrypted, expected_plaintext, + "Round-trip value mismatch for {}/{}", + ws, path + ); println!(" ✓ {}/{}: round-trip OK", ws, path); } @@ -522,10 +549,7 @@ mod tests { .get_secret("test-workspace", "u/test-user/other_secret") .await; - assert!( - cross_access.is_err(), - "Cross-workspace access should fail!" - ); + assert!(cross_access.is_err(), "Cross-workspace access should fail!"); println!("✓ Cross-workspace access correctly denied"); // Verify own workspace access works diff --git a/backend/windmill-common/tests/secret_backend_migration.rs b/backend/windmill-common/tests/secret_backend_migration.rs index 1915b90630..fba27ee260 100644 --- a/backend/windmill-common/tests/secret_backend_migration.rs +++ b/backend/windmill-common/tests/secret_backend_migration.rs @@ -23,19 +23,21 @@ use sqlx::{Pool, Postgres}; use windmill_common::error::Result; use windmill_common::secret_backend::{ - vault_oss::{migrate_secrets_to_database, migrate_secrets_to_vault, test_vault_connection, VaultBackend}, + vault_oss::{ + migrate_secrets_to_database, migrate_secrets_to_vault, test_vault_connection, VaultBackend, + }, SecretBackend, VaultSettings, }; fn test_vault_settings() -> VaultSettings { VaultSettings { - address: std::env::var("VAULT_ADDR").unwrap_or_else(|_| "http://127.0.0.1:8200".to_string()), + address: std::env::var("VAULT_ADDR") + .unwrap_or_else(|_| "http://127.0.0.1:8200".to_string()), mount_path: "windmill".to_string(), jwt_role: Some("windmill-secrets".to_string()), + jwt_mount_path: None, namespace: None, - token: Some( - std::env::var("VAULT_TOKEN").unwrap_or_else(|_| "test-root-token".to_string()), - ), + token: Some(std::env::var("VAULT_TOKEN").unwrap_or_else(|_| "test-root-token".to_string())), skip_ssl_verify: None, } } @@ -47,7 +49,11 @@ async fn test_vault_connection_works(db: Pool) { let settings = test_vault_settings(); let result = test_vault_connection(&settings, Some(&db)).await; - assert!(result.is_ok(), "Failed to connect to Vault: {:?}", result.err()); + assert!( + result.is_ok(), + "Failed to connect to Vault: {:?}", + result.err() + ); println!("✓ Successfully connected to Vault at {}", settings.address); } @@ -70,7 +76,10 @@ async fn test_migrate_db_to_vault(db: Pool) { .await .expect("Failed to query secrets"); - println!("Found {} secrets in database before migration:", secrets_before.len()); + println!( + "Found {} secrets in database before migration:", + secrets_before.len() + ); for s in &secrets_before { println!(" - {}/{}: {} chars", s.workspace_id, s.path, s.value.len()); } @@ -111,7 +120,10 @@ async fn test_migrate_db_to_vault(db: Pool) { secret.path, result.err() ); - println!(" ✓ {}/{} exists in Vault", secret.workspace_id, secret.path); + println!( + " ✓ {}/{} exists in Vault", + secret.workspace_id, secret.path + ); } println!("\n✓ Migration to Vault completed successfully"); @@ -133,8 +145,14 @@ async fn test_migrate_vault_to_db(db: Pool) { let to_vault_report = migrate_secrets_to_vault(&db, &settings) .await .expect("Initial migration to Vault failed"); - assert!(to_vault_report.migrated_count > 0, "No secrets to test with"); - println!(" Migrated {} secrets to Vault", to_vault_report.migrated_count); + assert!( + to_vault_report.migrated_count > 0, + "No secrets to test with" + ); + println!( + " Migrated {} secrets to Vault", + to_vault_report.migrated_count + ); // Clear the database values to simulate fresh migration back println!("\nClearing database secret values..."); @@ -150,7 +168,10 @@ async fn test_migrate_vault_to_db(db: Pool) { .fetch_one(&db) .await .expect("Failed to count cleared"); - println!(" Cleared {} secret values in database", cleared.count.unwrap_or(0)); + println!( + " Cleared {} secret values in database", + cleared.count.unwrap_or(0) + ); // Now migrate from Vault back to database println!("\nMigrating secrets from Vault to database..."); @@ -206,15 +227,14 @@ async fn test_full_round_trip_migration(db: Pool) { .expect("Failed to connect to Vault"); // Get original secrets - let original_secrets: std::collections::HashMap<(String, String), String> = sqlx::query!( - "SELECT workspace_id, path, value FROM variable WHERE is_secret = true" - ) - .fetch_all(&db) - .await - .expect("Failed to query original secrets") - .into_iter() - .map(|r| ((r.workspace_id, r.path), r.value)) - .collect(); + let original_secrets: std::collections::HashMap<(String, String), String> = + sqlx::query!("SELECT workspace_id, path, value FROM variable WHERE is_secret = true") + .fetch_all(&db) + .await + .expect("Failed to query original secrets") + .into_iter() + .map(|r| ((r.workspace_id, r.path), r.value)) + .collect(); println!("Original secrets: {} entries", original_secrets.len()); @@ -243,21 +263,23 @@ async fn test_full_round_trip_migration(db: Pool) { // Step 4: Verify round-trip integrity println!("\n=== Step 4: Verify round-trip integrity ==="); - let restored_secrets: std::collections::HashMap<(String, String), String> = sqlx::query!( - "SELECT workspace_id, path, value FROM variable WHERE is_secret = true" - ) - .fetch_all(&db) - .await - .expect("Failed to query restored secrets") - .into_iter() - .map(|r| ((r.workspace_id, r.path), r.value)) - .collect(); + let restored_secrets: std::collections::HashMap<(String, String), String> = + sqlx::query!("SELECT workspace_id, path, value FROM variable WHERE is_secret = true") + .fetch_all(&db) + .await + .expect("Failed to query restored secrets") + .into_iter() + .map(|r| ((r.workspace_id, r.path), r.value)) + .collect(); // Compare original and restored for ((ws, path), _original_value) in &original_secrets { let restored_value = restored_secrets .get(&(ws.clone(), path.clone())) - .expect(&format!("Secret {}/{} not found after round-trip", ws, path)); + .expect(&format!( + "Secret {}/{} not found after round-trip", + ws, path + )); // Note: Values might differ slightly due to encryption/decryption // but they should not be the cleared value @@ -266,7 +288,12 @@ async fn test_full_round_trip_migration(db: Pool) { "Secret {}/{} was not restored", ws, path ); - println!(" ✓ {}/{}: restored ({} chars)", ws, path, restored_value.len()); + println!( + " ✓ {}/{}: restored ({} chars)", + ws, + path, + restored_value.len() + ); } println!("\n✓ Full round-trip migration completed successfully!"); @@ -289,7 +316,10 @@ async fn test_workspace_isolation(db: Pool) { .await .expect("Migration failed"); - println!("Migrated {} secrets across workspaces", report.migrated_count); + println!( + "Migrated {} secrets across workspaces", + report.migrated_count + ); // Verify workspace isolation in Vault let vault_backend = VaultBackend::new(settings.clone()); @@ -309,13 +339,19 @@ async fn test_workspace_isolation(db: Pool) { let ws1_result: Result = vault_backend .get_secret("test-workspace", "u/test-user/db_password") .await; - assert!(ws1_result.is_ok(), "test-workspace secret should be accessible"); + assert!( + ws1_result.is_ok(), + "test-workspace secret should be accessible" + ); println!("✓ test-workspace secrets accessible"); let ws2_result: Result = vault_backend .get_secret("test-workspace-2", "u/test-user/other_secret") .await; - assert!(ws2_result.is_ok(), "test-workspace-2 secret should be accessible"); + assert!( + ws2_result.is_ok(), + "test-workspace-2 secret should be accessible" + ); println!("✓ test-workspace-2 secrets accessible"); println!("\n✓ Workspace isolation verified!"); diff --git a/backend/windmill-native-triggers/src/google/external.rs b/backend/windmill-native-triggers/src/google/external.rs index 29dd1c943f..616f8d3f63 100644 --- a/backend/windmill-native-triggers/src/google/external.rs +++ b/backend/windmill-native-triggers/src/google/external.rs @@ -323,9 +323,10 @@ impl Google { } /// Renew an expiring Google watch channel. - /// Rotates the webhook token (creating a new one with the same label), - /// stops the old channel and creates a new one with a fresh channel ID - /// (Google rejects reused channel IDs with `channelIdNotUnique`). + /// Rotates the webhook token (mints a fresh `ephemeral-webhook-google-{rd5}` label + /// and a 14-day expiration via `rotate_webhook_token`), stops the old channel and + /// creates a new one with a fresh channel ID (Google rejects reused channel IDs + /// with `channelIdNotUnique`). /// Returns (new_service_config, new_plaintext_token, old_token_hash). /// Callers should delete old_token_hash after successfully updating the trigger. pub async fn renew_channel( @@ -341,7 +342,13 @@ impl Google { .transpose()? .ok_or_else(|| Error::InternalErr("Missing service config".to_string()))?; - let rotated = match rotate_webhook_token(db, &trigger.webhook_token_hash).await? { + let rotated = match rotate_webhook_token( + db, + &trigger.webhook_token_hash, + ServiceName::Google, + ) + .await? + { Some(r) => r, None => { return Err(Error::InternalErr(format!( @@ -464,6 +471,115 @@ pub fn should_renew_channel(service_config: &serde_json::Value) -> bool { remaining_ms < renewal_window_ms } +enum RenewOutcome { + Renewed, + /// Another replica holds the lock, or the row was already renewed. + Skipped, +} + +/// Renew one Google watch channel under a row lock. +/// `sync_all_triggers` runs on every replica with no leader election — without +/// the lock, parallel renewals orphan the losers' new tokens and Google channels. +async fn try_renew_channel_locked( + handler: &Google, + db: &DB, + workspace_id: &str, + trigger: &NativeTrigger, +) -> Result { + let mut tx = db.begin().await?; + + let row = sqlx::query!( + r#" + SELECT service_config, webhook_token_hash + FROM native_trigger + WHERE workspace_id = $1 + AND service_name = $2 + AND external_id = $3 + FOR UPDATE SKIP LOCKED + "#, + workspace_id, + ServiceName::Google as ServiceName, + trigger.external_id, + ) + .fetch_optional(&mut *tx) + .await?; + + let Some(row) = row else { + return Ok(RenewOutcome::Skipped); + }; + + let Some(service_config) = row.service_config else { + // Anomalous: a Google trigger row should always carry a service_config. + tracing::warn!( + "Google trigger '{}' has NULL service_config — skipping renewal", + trigger.external_id + ); + return Ok(RenewOutcome::Skipped); + }; + + // Re-check after the lock — a contending replica may have just renewed. + if !should_renew_channel(&service_config) { + return Ok(RenewOutcome::Skipped); + } + + // Use freshly-read fields — webhook_token_hash may have rotated since list time. + let fresh_trigger = NativeTrigger { + service_config: Some(service_config), + webhook_token_hash: row.webhook_token_hash, + ..trigger.clone() + }; + + let (new_config, new_token, old_token_hash) = handler + .renew_channel(workspace_id, &fresh_trigger, db) + .await?; + + // Past this point a new Google channel exists. Any failure leaks it. + if let Err(e) = update_native_trigger_service_config( + &mut *tx, + workspace_id, + ServiceName::Google, + &trigger.external_id, + &new_config, + Some(&new_token), + ) + .await + { + tracing::error!( + "DB update failed after creating new Google channel for '{}' — channel orphaned in Google: {}", + trigger.external_id, + e + ); + return Err(e); + } + + if let Err(e) = tx.commit().await { + tracing::error!( + "Commit failed after creating new Google channel for '{}' — channel orphaned in Google: {}", + trigger.external_id, + e + ); + return Err(e.into()); + } + + // With the lock + rotation in place, the old token row must exist here. + // Ok(false) means a concurrent path deleted it (or the expiry sweep collected it). + match crate::delete_token_by_hash(db, &old_token_hash).await { + Ok(true) => {} + Ok(false) => tracing::warn!( + "Old webhook token already gone after renewal for '{}' (hash {})", + trigger.external_id, + old_token_hash + ), + Err(e) => tracing::warn!( + "Failed to delete old webhook token after channel renewal for '{}': {}", + trigger.external_id, + e + ), + } + + Ok(RenewOutcome::Renewed) +} + async fn renew_expiring_channels( handler: &Google, db: &DB, @@ -488,53 +604,25 @@ async fn renew_expiring_channels( workspace_id ); - match handler.renew_channel(workspace_id, trigger, db).await { - Ok((new_config, new_token, old_token_hash)) => { - match update_native_trigger_service_config( - db, - workspace_id, - ServiceName::Google, - &trigger.external_id, - &new_config, - Some(&new_token), - ) - .await - { - Ok(()) => { - // Trigger updated — clean up old token (best-effort) - if let Err(e) = crate::delete_token_by_hash(db, &old_token_hash).await { - tracing::warn!( - "Failed to delete old webhook token after channel renewal for {}: {}", - trigger.external_id, e - ); - } - tracing::info!( - "Renewed Google channel {} for '{}'", - trigger.external_id, - trigger.script_path - ); - synced.push(TriggerSyncInfo { - external_id: trigger.external_id.clone(), - script_path: trigger.script_path.clone(), - action: SyncAction::ConfigUpdated, - }); - } - Err(e) => { - tracing::error!( - "Failed to update DB after renewing Google channel {}: {}", - trigger.external_id, - e - ); - errors.push(SyncError { - resource_path: format!("workspace:{}", workspace_id), - error_message: format!( - "Failed to update DB after channel renewal for {}: {}", - trigger.external_id, e - ), - error_type: "channel_renewal_error".to_string(), - }); - } - } + match try_renew_channel_locked(handler, db, workspace_id, trigger).await { + Ok(RenewOutcome::Renewed) => { + tracing::info!( + "Renewed Google channel {} for '{}'", + trigger.external_id, + trigger.script_path + ); + synced.push(TriggerSyncInfo { + external_id: trigger.external_id.clone(), + script_path: trigger.script_path.clone(), + action: SyncAction::ConfigUpdated, + }); + } + Ok(RenewOutcome::Skipped) => { + // Expected outcome under SKIP LOCKED: contending replica or already-renewed row. + tracing::debug!( + "Skipped Google channel renewal for '{}': another replica is renewing or the row was already renewed", + trigger.external_id + ); } Err(e) => { tracing::error!( diff --git a/backend/windmill-native-triggers/src/handler.rs b/backend/windmill-native-triggers/src/handler.rs index 91a8b9531e..1b6aa83b92 100644 --- a/backend/windmill-native-triggers/src/handler.rs +++ b/backend/windmill-native-triggers/src/handler.rs @@ -1,7 +1,8 @@ use crate::{ decrypt_oauth_data, delete_native_trigger, delete_token_by_hash, get_native_trigger, list_native_triggers, rotate_webhook_token, store_native_trigger, update_native_trigger_error, - External, NativeTrigger, NativeTriggerConfig, NativeTriggerData, ServiceName, + webhook_token_label, External, NativeTrigger, NativeTriggerConfig, NativeTriggerData, + ServiceName, }; use axum::{ extract::{Path, Query}, @@ -18,7 +19,6 @@ use windmill_audit::{audit_oss::audit_log, ActionKind}; use windmill_common::{ db::UserDB, error::{Error, JsonResult, Result}, - utils::rd_string, DB, }; @@ -84,10 +84,13 @@ async fn new_webhook_token( let kind = if is_flow { "flows" } else { "scripts" }; let scopes = vec![format!("jobs:run:{kind}:{script_path}")]; - let label = format!("webhook-{}-{}", service_name.as_str(), rd_string(5)); + let label = webhook_token_label(service_name); + let expiration = service_name + .webhook_token_expiration() + .map(|d| chrono::Utc::now() + d); let token_config = NewToken::new( Some(label), - None, + expiration, None, Some(scopes), Some(workspace_id.to_owned()), @@ -255,8 +258,8 @@ async fn update_native_trigger_handler( tx = user_db.begin(&authed).await?; token } else { - // Same runnable — rotate the token keeping the same label - match rotate_webhook_token(&db, &existing.webhook_token_hash).await? { + // Same runnable — rotate the token (mints a fresh label + expiration) + match rotate_webhook_token(&db, &existing.webhook_token_hash, service_name).await? { Some(rotated) => { old_token_hash_to_delete = Some(rotated.old_token_hash); rotated.new_token diff --git a/backend/windmill-native-triggers/src/lib.rs b/backend/windmill-native-triggers/src/lib.rs index c871159b0f..743b9f8210 100644 --- a/backend/windmill-native-triggers/src/lib.rs +++ b/backend/windmill-native-triggers/src/lib.rs @@ -180,6 +180,17 @@ impl ServiceName { pub fn integration_service(&self) -> ServiceName { *self } + + /// How long webhook tokens for this service should remain valid. `None` = no expiry. + /// Google channels turn over on a tight schedule (24h Drive, 7d Calendar) — a finite + /// TTL lets `delete_expired_items` (`monitor.rs`) sweep orphaned tokens automatically. + /// Persistent-webhook services (Nextcloud, GitHub) return `None`. + pub fn webhook_token_expiration(&self) -> Option { + match self { + ServiceName::Google => Some(chrono::Duration::days(14)), + ServiceName::Nextcloud | ServiceName::Github => None, + } + } } impl std::fmt::Display for ServiceName { @@ -759,22 +770,23 @@ async fn update_oauth_token_resource( } } -/// Create a new webhook token that keeps the same label as the old one. -/// The old token is **not** deleted — callers must call `delete_token_by_hash` -/// on `old_token_hash` after the trigger row has been successfully updated. -/// This ensures the trigger keeps working if the external service call or -/// subsequent DB update fails. +/// Create a new webhook token, minting a fresh `ephemeral-webhook-{service}-{rd5}` +/// label and the per-service expiration (see `ServiceName::webhook_token_expiration`). +/// The old token is **not** deleted — callers must call `delete_token_by_hash` on +/// `old_token_hash` after the trigger row has been successfully updated. /// /// Returns `Ok(None)` if the old token no longer exists (e.g. manually deleted by user). -/// In that case, `renew_channel` returns an error which `renew_expiring_channels` writes -/// to the trigger's `error` column — visible in the UI so the user can re-create the trigger. -pub async fn rotate_webhook_token(db: &DB, old_token_hash: &str) -> Result> { +pub async fn rotate_webhook_token( + db: &DB, + old_token_hash: &str, + service_name: ServiceName, +) -> Result> { use windmill_common::auth::{hash_token, TOKEN_PREFIX_LEN}; use windmill_common::min_version::MIN_VERSION_SUPPORTS_TOKEN_HASH; use windmill_common::utils::rd_string; let old = match sqlx::query!( - "SELECT label, email, scopes, workspace_id, super_admin, owner, expiration FROM token WHERE token_hash = $1", + "SELECT email, scopes, workspace_id, super_admin, owner FROM token WHERE token_hash = $1", old_token_hash ) .fetch_optional(db) @@ -799,6 +811,11 @@ pub async fn rotate_webhook_token(db: &DB, old_token_hash: &str) -> Result Result, old.email, - old.label, + new_label, old.super_admin, old.scopes.as_deref(), old.workspace_id, old.owner, - old.expiration, + new_expiration, ) .execute(db) .await?; @@ -822,6 +839,19 @@ pub async fn rotate_webhook_token(db: &DB, old_token_hash: &str) -> Result String { + use windmill_common::utils::rd_string; + format!( + "ephemeral-webhook-{}-{}", + service_name.as_str(), + rd_string(5) + ) +} + pub struct RotatedToken { pub new_token: String, /// Hash of the old token — callers should delete this after the @@ -829,7 +859,10 @@ pub struct RotatedToken { pub old_token_hash: String, } -/// Delete a token from the token table using its hash (exact match). +/// Delete a token by hash. Returns `Ok(false)` when no row matched. +/// Some call sites legitimately race against expiry sweeps or concurrent deletes; +/// callers that consider 0-rows anomalous should log themselves at the appropriate +/// level rather than have this helper warn unconditionally. pub async fn delete_token_by_hash<'c, E: sqlx::Executor<'c, Database = Postgres>>( db: E, token_hash: &str, diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index 897f8e2ce0..52e6b09c6f 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -630,12 +630,45 @@ pub struct WrappedError { pub trait ValidableJson { fn is_valid_json(&self) -> bool; fn wm_labels(&self) -> Option>; + fn wm_failure(&self) -> Option; + fn result_metadata(&self) -> ResultMetadata; fn size(&self) -> usize; } -#[derive(serde::Deserialize)] -struct ResultLabels { - wm_labels: Vec, +/// The Windmill-specific markers we look for inside a job's result. +/// `wm_failure` retags a successful run as a failure with the +/// given message; `wm_labels` adds runtime labels to the job row. +#[derive(serde::Deserialize, Default, Debug, Clone)] +pub struct ResultMetadata { + pub wm_labels: Option>, + pub wm_failure: Option, +} + +/// Sentinel `error.name` we inject into a result when retagging a successful +/// run as a failure due to `wm_failure`. Used downstream to detect that +/// the result is already in the standard `{ error: { name, message }, ... }` +/// shape and must not be wrapped a second time by `WrappedError`. +pub const MANUAL_FAILURE_ERROR_NAME: &str = "ManualFailure"; + +/// Returns true when the result already carries our injected +/// `error: { name: "ManualFailure", ... }` marker — i.e. it was shaped by +/// `process_jc`'s wm_failure path. A real runtime failure whose raw +/// result happens to contain a `wm_failure` field but no such error +/// key returns false (and so still goes through the standard wrap path). +pub fn is_pre_shaped_wm_failure_result(result: &str) -> bool { + #[derive(serde::Deserialize)] + struct Marker { + error: Option, + } + #[derive(serde::Deserialize)] + struct NameOnly { + name: String, + } + serde_json::from_str::(result) + .ok() + .and_then(|m| m.error) + .map(|e| e.name == MANUAL_FAILURE_ERROR_NAME) + .unwrap_or(false) } impl ValidableJson for WrappedError { @@ -647,6 +680,14 @@ impl ValidableJson for WrappedError { None } + fn wm_failure(&self) -> Option { + None + } + + fn result_metadata(&self) -> ResultMetadata { + ResultMetadata::default() + } + fn size(&self) -> usize { 0 } @@ -658,9 +699,15 @@ impl ValidableJson for Box { } fn wm_labels(&self) -> Option> { - serde_json::from_str::(self.get()) - .ok() - .map(|r| r.wm_labels) + self.result_metadata().wm_labels + } + + fn wm_failure(&self) -> Option { + self.result_metadata().wm_failure + } + + fn result_metadata(&self) -> ResultMetadata { + serde_json::from_str::(self.get()).unwrap_or_default() } fn size(&self) -> usize { @@ -677,6 +724,14 @@ impl ValidableJson for Arc { T::wm_labels(&self) } + fn wm_failure(&self) -> Option { + T::wm_failure(&self) + } + + fn result_metadata(&self) -> ResultMetadata { + T::result_metadata(&self) + } + fn size(&self) -> usize { T::size(&self) } @@ -688,9 +743,15 @@ impl ValidableJson for serde_json::Value { } fn wm_labels(&self) -> Option> { - serde_json::from_value::(self.clone()) - .ok() - .map(|r| r.wm_labels) + self.result_metadata().wm_labels + } + + fn wm_failure(&self) -> Option { + self.result_metadata().wm_failure + } + + fn result_metadata(&self) -> ResultMetadata { + serde_json::from_value::(self.clone()).unwrap_or_default() } fn size(&self) -> usize { @@ -707,6 +768,14 @@ impl ValidableJson for Json { self.0.wm_labels() } + fn wm_failure(&self) -> Option { + self.0.wm_failure() + } + + fn result_metadata(&self) -> ResultMetadata { + self.0.result_metadata() + } + fn size(&self) -> usize { self.0.size() } @@ -742,16 +811,7 @@ where } } -pub async fn add_completed_job_error( - db: &Pool, - completed_job: &MiniCompletedJob, - mem_peak: i32, - canceled_by: Option, - e: serde_json::Value, - _worker_name: &str, - flow_is_done: bool, - duration: Option, -) -> Result { +async fn record_failure_metrics(completed_job: &MiniCompletedJob, _worker_name: &str) { #[cfg(feature = "prometheus")] register_metric( &WORKER_EXECUTION_FAILED, @@ -772,6 +832,64 @@ pub async fn add_completed_job_error( .await; otel_incr_worker_execution_failed(&completed_job.tag); +} + +/// Tag a completed job as a failure while storing the result as-is, without +/// the standard `WrappedError` `{ error: ... }` wrap. Use for jobs whose result +/// is already shaped (e.g. when `wm_failure` injected a top-level +/// `error` key, while preserving sibling fields like `windmill_status_code`). +/// +/// This is a worker-internal helper called by trusted result-processing code +/// after the worker has authenticated and pulled the job. Callers MUST verify +/// upstream auth (i.e. the job was legitimately pulled by this worker) — this +/// function performs no authorization check itself, mirroring the contract of +/// `add_completed_job_error`. +pub async fn add_completed_job_pre_shaped_failure( + db: &Pool, + completed_job: &MiniCompletedJob, + mem_peak: i32, + canceled_by: Option, + result: Json<&T>, + worker_name: &str, + flow_is_done: bool, + duration: Option, +) -> Result<(), Error> { + record_failure_metrics(completed_job, worker_name).await; + + tracing::error!( + "job {} in {} did not succeed (wm_failure)", + completed_job.id, + completed_job.workspace_id, + ); + let _ = add_completed_job( + db, + completed_job, + false, + false, + result, + None, + mem_peak, + canceled_by, + flow_is_done, + duration, + false, + ) + .warn_after_seconds(10) + .await?; + Ok(()) +} + +pub async fn add_completed_job_error( + db: &Pool, + completed_job: &MiniCompletedJob, + mem_peak: i32, + canceled_by: Option, + e: serde_json::Value, + worker_name: &str, + flow_is_done: bool, + duration: Option, +) -> Result { + record_failure_metrics(completed_job, worker_name).await; let result = WrappedError { error: e }; tracing::error!( diff --git a/backend/windmill-runtime-nativets/Cargo.toml b/backend/windmill-runtime-nativets/Cargo.toml index 417ec8de9d..edad615ae5 100644 --- a/backend/windmill-runtime-nativets/Cargo.toml +++ b/backend/windmill-runtime-nativets/Cargo.toml @@ -28,9 +28,9 @@ 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 -deno_runtime.workspace = true winapi.workspace = true itertools.workspace = true @@ -60,6 +60,7 @@ deno_ast.workspace = true deno_tls.workspace = true deno_permissions.workspace = true deno_io.workspace = true -deno_runtime.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 2f4ec8c38d..8262827815 100644 --- a/backend/windmill-runtime-nativets/build.rs +++ b/backend/windmill-runtime-nativets/build.rs @@ -1,3 +1,6 @@ +use deno_ast::{MediaType, ParseParams}; +use deno_core::{ModuleCodeString, ModuleName, SourceMapData}; +use deno_error::JsErrorBox; use deno_fetch::FetchPermissions; use deno_net::NetPermissions; use deno_web::{BlobStore, TimersPermission}; @@ -22,10 +25,30 @@ impl FetchPermissions for PermissionsContainer { #[inline(always)] fn check_read<'a>( &mut self, - _resolved: bool, - _p: &'a std::path::Path, + _path: Cow<'a, Path>, _api_name: &str, - ) -> Result, deno_io::fs::FsError> { + _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") } } @@ -38,17 +61,17 @@ impl TimersPermission for PermissionsContainer { } impl NetPermissions for PermissionsContainer { - fn check_read<'a>( + fn check_read( &mut self, - _p: &'a str, + _p: &str, _api_name: &str, ) -> Result { unreachable!("snapshotting") } - fn check_write<'a>( + fn check_write( &mut self, - _p: &'a str, + _p: &str, _api_name: &str, ) -> Result { unreachable!("snapshotting") @@ -64,10 +87,19 @@ impl NetPermissions for PermissionsContainer { fn check_write_path<'a>( &mut self, - _: &'a Path, - _: &str, + _p: Cow<'a, Path>, + _api_name: &str, ) -> Result, deno_permissions::PermissionCheckError> { - todo!() + unreachable!("snapshotting") + } + + fn check_vsock( + &mut self, + _cid: u32, + _port: u32, + _api_name: &str, + ) -> Result<(), deno_permissions::PermissionCheckError> { + unreachable!("snapshotting") } } @@ -77,22 +109,87 @@ deno_core::extension!( esm = ["src/runtime.js"], ); +// `extension_transpiler` callback for `deno_core::snapshot::create_snapshot`. +// +// Specialized to our snapshot's inputs. Of the seven deno_* extensions +// we register via `init()`, six ship pre-built `.js` files +// in their `esm` lists (webidl/url/console/web/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: +// - `.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. +// +// 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. +// +// 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). +fn maybe_transpile_source( + name: ModuleName, + source: ModuleCodeString, +) -> Result<(ModuleCodeString, Option), JsErrorBox> { + let media_type = MediaType::from_path(Path::new(&name)); + match media_type { + MediaType::JavaScript => return Ok((source, None)), + MediaType::TypeScript => {} + _ => panic!("unexpected media type {media_type:?} for {name} during snapshot build"), + } + + let parsed = deno_ast::parse_module(ParseParams { + specifier: deno_core::url::Url::parse(&name).unwrap(), + text: source.into(), + media_type, + capture_tokens: false, + scope_analysis: false, + maybe_syntax: None, + }) + .unwrap_or_else(|e| panic!("snapshot transpile: parse failed for {name}: {e}")); + + let transpiled = parsed + .transpile( + &deno_ast::TranspileOptions { + imports_not_used_as_values: deno_ast::ImportsNotUsedAsValues::Remove, + ..Default::default() + }, + &deno_ast::TranspileModuleOptions::default(), + &deno_ast::EmitOptions::default(), + ) + .unwrap_or_else(|e| panic!("snapshot transpile: emit failed for {name}: {e}")) + .into_source(); + + Ok((transpiled.text.into(), None)) +} + 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_ops_and_esm(), - deno_webidl::deno_webidl::init_ops_and_esm(), - deno_url::deno_url::init_ops_and_esm(), - deno_console::deno_console::init_ops_and_esm(), - deno_web::deno_web::init_ops_and_esm::( - Arc::new(BlobStore::default()), - None, - ), - deno_fetch::deno_fetch::init_ops_and_esm::(Default::default()), - deno_net::deno_net::init_ops_and_esm::(None, None), - fetch::init_ops_and_esm(), + 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_fetch::deno_fetch::init::(Default::default()), + deno_net::deno_net::init::(None, None), + fetch::init(), ]; // Build the file path to the snapshot. @@ -105,7 +202,7 @@ fn main() { cargo_manifest_dir: env!("CARGO_MANIFEST_DIR"), startup_snapshot: None, extension_transpiler: Some(std::rc::Rc::new(|specifier, source| { - deno_runtime::transpile::maybe_transpile_source(specifier, source) + maybe_transpile_source(specifier, source) })), extensions: exts, with_runtime_cb: None, diff --git a/backend/windmill-runtime-nativets/src/lib.rs b/backend/windmill-runtime-nativets/src/lib.rs index dda57ae708..52e23f11da 100644 --- a/backend/windmill-runtime-nativets/src/lib.rs +++ b/backend/windmill-runtime-nativets/src/lib.rs @@ -15,6 +15,9 @@ mod dedicated; pub use dedicated::{ExecutingIsolate, PrewarmedIsolate, PrewarmedResult}; +#[cfg(test)] +mod smoke_tests; + use std::{ borrow::Cow, cell::RefCell, @@ -47,6 +50,28 @@ use windmill_common::error::Error; use windmill_common::result_stream::append_result_stream_db; 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). +// +// 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. +deno_core::extension!( + fetch, + esm_entry_point = "ext:fetch/src/runtime.js", + esm = ["src/runtime.js"], +); + // ── Permission container ───────────────────────────────────────────── pub struct PermissionsContainer; @@ -64,11 +89,31 @@ impl FetchPermissions for PermissionsContainer { #[inline(always)] fn check_read<'a>( &mut self, - _resolved: bool, - p: &'a std::path::Path, + path: Cow<'a, std::path::Path>, _api_name: &str, - ) -> Result, deno_io::fs::FsError> { - Ok(Cow::Borrowed(p)) + _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> { + Ok(()) } } @@ -80,17 +125,17 @@ impl TimersPermission for PermissionsContainer { } impl NetPermissions for PermissionsContainer { - fn check_read<'a>( + fn check_read( &mut self, - p: &'a str, + p: &str, _api_name: &str, ) -> Result { Ok(PathBuf::from(p)) } - fn check_write<'a>( + fn check_write( &mut self, - p: &'a str, + p: &str, _api_name: &str, ) -> Result { Ok(PathBuf::from(p)) @@ -106,10 +151,19 @@ impl NetPermissions for PermissionsContainer { fn check_write_path<'a>( &mut self, - p: &'a std::path::Path, + p: Cow<'a, std::path::Path>, _api_name: &str, - ) -> Result, deno_permissions::PermissionCheckError> { - Ok(Cow::Borrowed(p)) + ) -> Result, deno_permissions::PermissionCheckError> { + Ok(p) + } + + fn check_vsock( + &mut self, + _cid: u32, + _port: u32, + _api_name: &str, + ) -> Result<(), deno_permissions::PermissionCheckError> { + Ok(()) } } @@ -381,7 +435,7 @@ pub(crate) fn create_nativets_runtime( let fetch_options = deno_fetch::Options { root_cert_store_provider: None, user_agent: ann.useragent.unwrap_or_else(|| "windmill/beta".to_string()), - proxy: ann.proxy.map(|x| deno_tls::Proxy { + proxy: ann.proxy.map(|x| deno_tls::Proxy::Http { url: x.0, basic_auth: x .1 @@ -391,13 +445,14 @@ pub(crate) fn create_nativets_runtime( }; let exts: Vec = vec![ - deno_telemetry::deno_telemetry::init_ops(), - deno_webidl::deno_webidl::init_ops(), - deno_url::deno_url::init_ops(), - deno_console::deno_console::init_ops(), - deno_web::deno_web::init_ops::(Arc::new(BlobStore::default()), None), - deno_fetch::deno_fetch::init_ops::(fetch_options), - deno_net::deno_net::init_ops::(None, None), + 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_fetch::deno_fetch::init::(fetch_options), + deno_net::deno_net::init::(None, None), + fetch::init(), ext, ]; diff --git a/backend/windmill-runtime-nativets/src/smoke_tests.rs b/backend/windmill-runtime-nativets/src/smoke_tests.rs new file mode 100644 index 0000000000..20675ee2a7 --- /dev/null +++ b/backend/windmill-runtime-nativets/src/smoke_tests.rs @@ -0,0 +1,280 @@ +//! Opt-in smoke tests for the nativets V8 runtime. +//! +//! Exercise the deno_core / deno_ast / swc surface (TypeScript transpile, +//! fetch, timers, URL, structuredClone, error propagation, concurrent +//! isolates, large payload roundtrip) that the standard worker-level +//! nativets tests in `backend/tests/worker.rs` don't reach — those tests +//! validate value passing through the job queue, but not the JS API +//! surface a deno_core bump would actually move. +//! +//! These tests are `#[ignore]`'d so the regular `cargo test` flow doesn't +//! pay their cost (each spawns a V8 isolate; some hit the network). Run +//! when changing the `deno_core` / `deno_ast` / `deno_runtime` / `swc_*` +//! pins in `backend/Cargo.toml`: +//! +//! cargo test -p windmill-runtime-nativets smoke -- --ignored +//! +//! Tests prefixed `smoke_net_` hit the public internet (httpbin.org, +//! example.com) and will fail if the runner has no egress. Skip them +//! locally with `cargo test -p windmill-runtime-nativets smoke -- --ignored --skip smoke_net_`. + +use crate::{transpile_ts, NativeAnnotation, PrewarmedIsolate, PrewarmedResult}; + +/// Compile a TS snippet, run it through a fresh isolate with the given +/// positional args, and return the isolate's result + captured logs. +async fn run_ts(ts: &str, arg_names: &[&str], args: serde_json::Value) -> PrewarmedResult { + let js = transpile_ts(ts.to_string()).expect("transpile_ts failed"); + let ann = NativeAnnotation { useragent: None, proxy: None }; + let arg_names: Vec = arg_names.iter().map(|s| s.to_string()).collect(); + let mut iso = PrewarmedIsolate::spawn(String::new(), js, ann, arg_names, None); + iso.wait_ready().await.expect("isolate failed to pre-warm"); + iso.start_execution(args.to_string()) + .wait() + .await + .expect("isolate execution panicked") +} + +fn unwrap_value(r: &PrewarmedResult) -> serde_json::Value { + let raw = r.result.as_ref().expect("script returned an error"); + serde_json::from_str(raw.get()).expect("result not valid JSON") +} + +// ----------------------------------------------------------------------------- +// Local (no network) — these still need V8 / deno_core ops to be wired. +// ----------------------------------------------------------------------------- + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore = "deno_core upgrade smoke; run with --ignored"] +async fn smoke_basic_value_passing() { + let ts = r#" +export async function main(x: number): Promise { + return x + 1; +} +"#; + let r = run_ts(ts, &["x"], serde_json::json!({"x": 41})).await; + assert_eq!(unwrap_value(&r), serde_json::json!(42)); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore = "deno_core upgrade smoke; run with --ignored"] +async fn smoke_transpile_enum_and_union() { + // Enums + discriminated union + as-cast exercise the swc_ecma_ast + + // swc_ecma_parser TS-syntax paths the bare value tests don't. + let ts = r#" +enum Direction { Up = "U", Down = "D" } +type Msg = { kind: "move"; dir: Direction } | { kind: "stop" }; +export async function main(): Promise { + const msgs: Msg[] = [ + { kind: "move", dir: Direction.Up }, + { kind: "stop" }, + { kind: "move", dir: Direction.Down }, + ]; + return msgs.map(m => m.kind === "move" ? m.dir : "_").join(","); +} +"#; + let r = run_ts(ts, &[], serde_json::json!({})).await; + assert_eq!(unwrap_value(&r), serde_json::json!("U,_,D")); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore = "deno_core upgrade smoke; run with --ignored"] +async fn smoke_set_timeout_and_promise_all() { + // setTimeout lives in deno_web; Promise.all hits the V8 microtask + // queue. A bump that breaks timer-op registration or microtask drain + // would surface here (script would hang or return wrong order). + let ts = r#" +export async function main(): Promise { + const delays = [40, 10, 20, 30]; + return await Promise.all(delays.map(d => + new Promise(resolve => setTimeout(() => resolve(d), d)) + )); +} +"#; + let r = run_ts(ts, &[], serde_json::json!({})).await; + // Promise.all preserves input order regardless of resolution order. + assert_eq!(unwrap_value(&r), serde_json::json!([40, 10, 20, 30])); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore = "deno_core upgrade smoke; run with --ignored"] +async fn smoke_url_and_searchparams() { + // deno_url surface: URL ctor, URLSearchParams parsing + iteration. + let ts = r#" +export async function main(): Promise<{ host: string; pairs: [string, string][] }> { + const u = new URL("https://example.com:8443/path?b=2&a=1&a=3"); + const pairs: [string, string][] = []; + for (const [k, v] of u.searchParams) pairs.push([k, v]); + return { host: u.host, pairs }; +} +"#; + let r = run_ts(ts, &[], serde_json::json!({})).await; + assert_eq!( + unwrap_value(&r), + serde_json::json!({ + "host": "example.com:8443", + "pairs": [["b", "2"], ["a", "1"], ["a", "3"]], + }), + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore = "deno_core upgrade smoke; run with --ignored"] +async fn smoke_web_blob_btoa_atob() { + // deno_web surface: Blob, atob/btoa. `structuredClone` is *not* wired + // into the nativets global (the deno_web binding doesn't expose it + // here) — if that's ever changed, extend this test to cover it. + let ts = r#" +export async function main(): Promise<{ b64: string; round_trip: string; size: number }> { + const blob = new Blob(["hello"], { type: "text/plain" }); + const b64 = btoa("hello"); + const round_trip = atob(b64); + return { b64, round_trip, size: blob.size }; +} +"#; + let r = run_ts(ts, &[], serde_json::json!({})).await; + assert_eq!( + unwrap_value(&r), + serde_json::json!({ + "b64": "aGVsbG8=", + "round_trip": "hello", + "size": 5, + }), + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore = "deno_core upgrade smoke; run with --ignored"] +async fn smoke_large_payload_roundtrip() { + // ~512 KB string in and out — exercises arg encoding + result + // serialization through the deno_core <-> host op boundary at sizes + // an op-table change could break. + let big_in: String = "a".repeat(512 * 1024); + let ts = r#" +export async function main(s: string): Promise<{ in_len: number; out: string }> { + if (typeof s !== "string") throw new Error(`expected string, got ${typeof s}`); + return { in_len: s.length, out: "b".repeat(512 * 1024) }; +} +"#; + let r = run_ts(ts, &["s"], serde_json::json!({"s": big_in})).await; + let v = unwrap_value(&r); + assert_eq!(v.get("in_len").and_then(|x| x.as_u64()), Some(512 * 1024)); + let out_len = v + .get("out") + .and_then(|x| x.as_str()) + .map(|s| s.len()) + .unwrap_or(0); + assert_eq!(out_len, 512 * 1024); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore = "deno_core upgrade smoke; run with --ignored"] +async fn smoke_error_propagation_with_message() { + // Throwing a typed Error must surface as PrewarmedResult::Err with + // the original message. A deno_core bump that changes the host-side + // error wrapping would lose this contract. + let ts = r#" +export async function main(): Promise { + throw new Error("nativets_smoke_marker_xyz"); +} +"#; + let r = run_ts(ts, &[], serde_json::json!({})).await; + let err = r.result.expect_err("expected script to fail"); + assert!( + err.contains("nativets_smoke_marker_xyz"), + "thrown error message did not reach result: {err}", + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[ignore = "deno_core upgrade smoke; run with --ignored"] +async fn smoke_concurrent_isolates() { + // Spawn N isolates in parallel from the same tokio runtime. Each + // PrewarmedIsolate uses spawn_blocking + a fresh V8 isolate. + // Catches isolate-setup races (V8_ISOLATE_CREATE_LOCK ordering) and + // any per-isolate state that a deno_core bump could break under + // concurrency. + let ts = r#" +export async function main(i: number): Promise { + return i * 10; +} +"#; + let js = transpile_ts(ts.to_string()).expect("transpile_ts failed"); + + const N: i64 = 8; + let mut handles = Vec::with_capacity(N as usize); + for i in 0..N { + let js = js.clone(); + let h = tokio::spawn(async move { + let ann = NativeAnnotation { useragent: None, proxy: None }; + let mut iso = + PrewarmedIsolate::spawn(String::new(), js, ann, vec!["i".to_string()], None); + iso.wait_ready().await.expect("pre-warm failed"); + let res = iso + .start_execution(serde_json::json!({"i": i}).to_string()) + .wait() + .await + .expect("isolate panicked"); + res.result.expect("script errored") + }); + handles.push(h); + } + + let mut got: Vec = Vec::with_capacity(N as usize); + for h in handles { + let raw = h.await.expect("join failed"); + let v: serde_json::Value = serde_json::from_str(raw.get()).expect("not JSON"); + got.push(v.as_i64().unwrap_or(-1)); + } + got.sort(); + let expected: Vec = (0..N).map(|i| i * 10).collect(); + assert_eq!(got, expected); +} + +// ----------------------------------------------------------------------------- +// Network — actually exercise deno_fetch end-to-end. Skip in air-gapped CI +// with `--skip smoke_net_`. +// ----------------------------------------------------------------------------- + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore = "deno_core upgrade smoke (network); run with --ignored"] +async fn smoke_net_fetch_example_com() { + // example.com is one of the most stable hosts on the internet and + // returns a tiny known-text body, so we can both assert "fetch works" + // and "the response body parses correctly through deno_fetch". + let ts = r#" +export async function main(): Promise<{ status: number; has_marker: boolean }> { + const r = await fetch("https://example.com/"); + const body = await r.text(); + return { status: r.status, has_marker: body.includes("Example Domain") }; +} +"#; + let r = run_ts(ts, &[], serde_json::json!({})).await; + let v = unwrap_value(&r); + assert_eq!(v.get("status").and_then(|x| x.as_u64()), Some(200)); + assert_eq!(v.get("has_marker"), Some(&serde_json::json!(true))); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore = "deno_core upgrade smoke (network); run with --ignored"] +async fn smoke_net_fetch_json_and_headers() { + // httpbin.org/anything echoes request metadata back as JSON, so we + // can verify: deno_fetch sends custom headers, parses JSON response, + // and propagates query params end-to-end. + let ts = r#" +export async function main(): Promise<{ ua: string; arg: string }> { + const r = await fetch("https://httpbin.org/anything?nativets=ok", { + headers: { "x-windmill-smoke": "1" }, + }); + if (!r.ok) throw new Error(`status ${r.status}`); + const j: any = await r.json(); + return { + ua: j.headers["X-Windmill-Smoke"] ?? "", + arg: j.args.nativets ?? "", + }; +} +"#; + let r = run_ts(ts, &[], serde_json::json!({})).await; + let v = unwrap_value(&r); + assert_eq!(v.get("ua").and_then(|x| x.as_str()), Some("1")); + assert_eq!(v.get("arg").and_then(|x| x.as_str()), Some("ok")); +} diff --git a/backend/windmill-store/src/resources.rs b/backend/windmill-store/src/resources.rs index 6b169a0c0d..8aa88bad86 100644 --- a/backend/windmill-store/src/resources.rs +++ b/backend/windmill-store/src/resources.rs @@ -1238,10 +1238,39 @@ async fn delete_resources_bulk( .await?; if let Some(res_data) = trash_resource { + // Per-resource linked vars so each resource's trash entry carries + // exactly the variables that vanished with it (matching the + // single-delete shape: trash_data["linked_variables"]). + let mut this_linked: Vec = Vec::new(); if let Some(value) = res_data.get("value") { - collect_var_refs(value, &mut linked_var_paths); + collect_var_refs(value, &mut this_linked); + } + this_linked.sort(); + this_linked.dedup(); + + let trash_linked_vars: Vec = if this_linked.is_empty() { + Vec::new() + } else { + let placeholders: Vec = this_linked + .iter() + .enumerate() + .map(|(i, _)| format!("${}", i + 2)) + .collect(); + let query = format!( + "SELECT to_jsonb(t) FROM variable t WHERE workspace_id = $1 AND path IN ({})", + placeholders.join(", ") + ); + let mut q = sqlx::query_scalar::<_, serde_json::Value>(&query).bind(&w_id); + for var_path in &this_linked { + q = q.bind(var_path); + } + q.fetch_all(&mut *tx).await? + }; + + let mut trash_data = serde_json::json!({"row": res_data}); + if !trash_linked_vars.is_empty() { + trash_data["linked_variables"] = serde_json::Value::Array(trash_linked_vars); } - let trash_data = serde_json::json!({"row": res_data}); windmill_common::trashbin::move_to_trash( &mut *tx, &w_id, @@ -1251,6 +1280,8 @@ async fn delete_resources_bulk( &authed.username, ) .await?; + + linked_var_paths.extend(this_linked); } } linked_var_paths.sort(); diff --git a/backend/windmill-store/src/variables.rs b/backend/windmill-store/src/variables.rs index a9f8734647..c50049fcd6 100644 --- a/backend/windmill-store/src/variables.rs +++ b/backend/windmill-store/src/variables.rs @@ -755,6 +755,17 @@ async fn delete_variables_bulk( ) .fetch_all(&mut *tx) .await?; + // Mirror single delete_variable: clean the linked-resource ws_specific + // markers BEFORE deleting the resource rows so they don't survive as + // orphans. A resource later created at the same path would otherwise + // inherit a stale ws_specific flag. + sqlx::query!( + "DELETE FROM ws_specific WHERE workspace_id = $1 AND item_kind = 'resource' AND path = ANY($2)", + w_id, + &deleted_paths + ) + .execute(&mut *tx) + .await?; sqlx::query!( "DELETE FROM resource WHERE path = ANY($1) AND workspace_id = $2", &deleted_paths, @@ -1019,6 +1030,20 @@ async fn update_variable( ) .execute(&mut *tx) .await?; + + // The linked resource at the same path is renamed above; move + // its ws_specific 'resource' marker too so an explicitly-flagged + // resource doesn't lose its ws_specific status on rename and + // doesn't leave a stale marker at the old path. Symmetric with + // update_resource's rename block. + sqlx::query!( + "UPDATE ws_specific SET path = $1 WHERE workspace_id = $2 AND item_kind = 'resource' AND path = $3", + npath, + w_id, + path + ) + .execute(&mut *tx) + .await?; } } diff --git a/backend/windmill-worker/Cargo.toml b/backend/windmill-worker/Cargo.toml index ad3d87d8df..5ebcb0b370 100644 --- a/backend/windmill-worker/Cargo.toml +++ b/backend/windmill-worker/Cargo.toml @@ -39,7 +39,7 @@ ruby = ["dep:windmill-parser-ruby"] rlang = ["dep:windmill-parser-r"] duckdb = ["dep:libloading"] quickjs = ["windmill-jseval/quickjs"] -bedrock = ["windmill-ai/bedrock", "dep:aws-sdk-bedrockruntime", "dep:aws-config", "dep:aws-credential-types", "dep:aws-smithy-types"] +bedrock = ["windmill-ai/bedrock"] [dependencies] windmill-ai = { workspace = true, default-features = false } @@ -71,10 +71,6 @@ windmill-parser-sql.workspace = true windmill-parser-graphql.workspace = true windmill-parser-php = { workspace = true, optional = true } windmill-git-sync.workspace = true -aws-sdk-bedrockruntime = { workspace = true, optional = true } -aws-config = { workspace = true, optional = true } -aws-credential-types = { workspace = true, optional = true } -aws-smithy-types = { workspace = true, optional = true } flume.workspace = true sqlx.workspace = true uuid.workspace = true @@ -93,6 +89,7 @@ itertools.workspace = true regex.workspace = true prometheus = { workspace = true, optional = true } lazy_static.workspace = true +quick_cache.workspace = true chrono.workspace = true dotenv.workspace = true rand.workspace = true # TODO: Remove. only used by token creation hack. diff --git a/backend/windmill-worker/src/ai/mod.rs b/backend/windmill-worker/src/ai/mod.rs index 4ad67b9e6e..24e877ab13 100644 --- a/backend/windmill-worker/src/ai/mod.rs +++ b/backend/windmill-worker/src/ai/mod.rs @@ -1,10 +1,6 @@ // AI executor module structure // This module will contain all AI-related execution logic -pub mod image_handler; -pub mod providers; pub mod query_builder; -pub mod sse; pub mod tools; -pub mod types; pub mod utils; diff --git a/backend/windmill-worker/src/ai/providers/mod.rs b/backend/windmill-worker/src/ai/providers/mod.rs deleted file mode 100644 index f558c28289..0000000000 --- a/backend/windmill-worker/src/ai/providers/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod anthropic; -#[cfg(feature = "bedrock")] -pub mod bedrock; -pub mod google_ai; -pub mod openai; -pub mod openrouter; -pub mod other; diff --git a/backend/windmill-worker/src/ai/query_builder.rs b/backend/windmill-worker/src/ai/query_builder.rs index d74c45c098..fe8a3b9b00 100644 --- a/backend/windmill-worker/src/ai/query_builder.rs +++ b/backend/windmill-worker/src/ai/query_builder.rs @@ -1,42 +1,9 @@ use async_trait::async_trait; +use windmill_ai::{query_builder::StreamEventSink, types::*}; use windmill_common::{error::Error, worker::Connection}; use windmill_queue::MiniPulledJob; -use crate::{ - ai::{ - providers::{ - anthropic::AnthropicQueryBuilder, google_ai::GoogleAIQueryBuilder, - openai::OpenAIQueryBuilder, openrouter::OpenRouterQueryBuilder, - other::OtherQueryBuilder, - }, - types::*, - }, - job_logger::append_result_stream, -}; - -// Re-export from windmill_ai -pub use windmill_ai::query_builder::{ - BuildRequestArgs, ParsedResponse, QueryBuilder, StreamEventSink, -}; - -/// Factory function to create the appropriate query builder for a provider -pub fn create_query_builder(provider: &ProviderWithResource) -> Box { - use windmill_ai::ai_providers::AIProvider; - - match provider.kind { - AIProvider::GoogleAI => { - Box::new(GoogleAIQueryBuilder::new(provider.get_platform().clone())) - } - AIProvider::OpenAI => Box::new(OpenAIQueryBuilder::new(provider.kind.clone())), - AIProvider::Anthropic => Box::new(AnthropicQueryBuilder::new( - provider.kind.clone(), - provider.get_platform().clone(), - provider.get_enable_1m_context(), - )), - AIProvider::OpenRouter => Box::new(OpenRouterQueryBuilder::new()), - _ => Box::new(OtherQueryBuilder::new(provider.kind.clone())), - } -} +use crate::job_logger::append_result_stream; /// Processes streaming events by persisting them to the database. /// Implements StreamEventSink so it can be passed to QueryBuilder methods. diff --git a/backend/windmill-worker/src/ai/tools.rs b/backend/windmill-worker/src/ai/tools.rs index c92fc2a94e..fcc9cdf3c9 100644 --- a/backend/windmill-worker/src/ai/tools.rs +++ b/backend/windmill-worker/src/ai/tools.rs @@ -1,6 +1,4 @@ -use crate::ai::query_builder::{StreamEventProcessor, StreamEventSink}; -use crate::ai::types::McpToolSource; -use crate::ai::types::*; +use crate::ai::query_builder::StreamEventProcessor; use crate::ai::utils::{ add_message_to_conversation, execute_mcp_tool, get_step_name_from_flow, is_completed_input_transform, update_flow_status_module_with_actions, @@ -20,7 +18,7 @@ use mappable_rc::Marc; use serde_json::value::RawValue; use std::{collections::HashMap, sync::Arc}; use uuid::Uuid; -use windmill_ai::ai_types::OpenAIToolCall; +use windmill_ai::{ai_types::OpenAIToolCall, query_builder::StreamEventSink, types::*}; use windmill_common::jobs::JobPayload; #[cfg(feature = "mcp")] diff --git a/backend/windmill-worker/src/ai/types.rs b/backend/windmill-worker/src/ai/types.rs deleted file mode 100644 index 57619f0dd9..0000000000 --- a/backend/windmill-worker/src/ai/types.rs +++ /dev/null @@ -1,2 +0,0 @@ -// Re-export all types from windmill_ai::types -pub use windmill_ai::types::*; diff --git a/backend/windmill-worker/src/ai/utils.rs b/backend/windmill-worker/src/ai/utils.rs index f1e8fdd22f..74e75ef0a5 100644 --- a/backend/windmill-worker/src/ai/utils.rs +++ b/backend/windmill-worker/src/ai/utils.rs @@ -1,5 +1,3 @@ -pub use crate::ai::types::McpToolSource; -use crate::ai::types::ToolDef; use anyhow::Context; use serde_json::value::RawValue; use sqlx::types::Json; @@ -8,7 +6,7 @@ use std::{ sync::Arc, }; use uuid::Uuid; -use windmill_ai::ai_providers::AIProvider; +use windmill_ai::types::*; use windmill_common::flows::FlowModuleValue; use windmill_common::{ db::DB, @@ -24,7 +22,7 @@ use windmill_common::{ use windmill_mcp::{McpClient, McpResource, McpTool}; use windmill_queue::{flow_status::get_step_of_flow_status, MiniPulledJob}; -use crate::{ai::types::*, parse_sig_of_lang}; +use crate::parse_sig_of_lang; pub fn parse_raw_script_schema( content: &str, @@ -323,11 +321,6 @@ pub fn get_step_name_from_flow( ) } -/// AWS Bedrock do not handle structured output query param, so we use a tool for structured output. Same for every Claude models. -pub fn should_use_structured_output_tool(provider: &AIProvider, model: &str) -> bool { - model.contains("claude") || provider == &AIProvider::AWSBedrock -} - /// Cleanup MCP clients by gracefully shutting down connections #[cfg(feature = "mcp")] pub async fn cleanup_mcp_clients(mcp_clients: HashMap>) { @@ -713,21 +706,3 @@ pub fn any_tool_needs_previous_result(tools: &[Tool]) -> bool { false }) } - -/// Extract text content from OpenAIContent, joining parts with space if multiple -pub fn extract_text_content(content: &OpenAIContent) -> String { - match content { - OpenAIContent::Text(text) => text.clone(), - OpenAIContent::Parts(parts) => parts - .iter() - .filter_map(|p| { - if let ContentPart::Text { text } = p { - Some(text.as_str()) - } else { - None - } - }) - .collect::>() - .join(""), - } -} diff --git a/backend/windmill-worker/src/ai_executor.rs b/backend/windmill-worker/src/ai_executor.rs index 03153df0a4..dae4f277f2 100644 --- a/backend/windmill-worker/src/ai_executor.rs +++ b/backend/windmill-worker/src/ai_executor.rs @@ -1,12 +1,10 @@ -#[cfg(feature = "bedrock")] -use crate::ai::providers::bedrock::check_env_credentials; use crate::ai::tools::{execute_tool_calls, ToolAbortHandles, ToolExecutionContext}; use crate::ai::utils::{ add_message_to_conversation, any_tool_needs_previous_result, cleanup_mcp_clients, filter_schema_by_input_transforms, find_unique_tool_name, get_flow_context, get_flow_job_runnable_and_raw_flow, get_step_name_from_flow, load_mcp_tools, - parse_raw_script_schema, should_use_structured_output_tool, - update_flow_status_module_with_actions, update_flow_status_module_with_actions_success, + parse_raw_script_schema, update_flow_status_module_with_actions, + update_flow_status_module_with_actions_success, }; use crate::memory_oss::{read_from_memory, write_to_memory}; use crate::worker_flow::{get_previous_job_result, get_transform_context}; @@ -15,12 +13,21 @@ use regex::Regex; use serde_json::value::RawValue; use std::{collections::HashMap, sync::Arc}; use uuid::Uuid; +#[cfg(feature = "bedrock")] +use windmill_ai::ai_bedrock::check_env_credentials; #[cfg(feature = "mcp")] use windmill_mcp::McpClient; #[cfg(not(feature = "mcp"))] use crate::ai::tools::McpClientStub as McpClient; -use windmill_ai::ai_providers::AIProvider; +use windmill_ai::{ + ai_providers::AIProvider, + image_handler::upload_image_to_s3, + providers::create_query_builder, + query_builder::{BuildRequestArgs, ParsedResponse}, + types::*, + utils::{should_use_structured_output_tool, AI_HTTP_HEADERS}, +}; use windmill_common::{ cache, client::AuthedClient, @@ -38,13 +45,7 @@ use windmill_common::{ use windmill_queue::{cancel_single_job, CanceledBy, MiniPulledJob}; use crate::{ - ai::{ - image_handler::upload_image_to_s3, - query_builder::{ - create_query_builder, BuildRequestArgs, ParsedResponse, StreamEventProcessor, - }, - types::*, - }, + ai::query_builder::StreamEventProcessor, common::{build_args_map, resolve_job_timeout, OccupancyMetrics, StreamNotifier}, handle_child::{run_future_with_polling_update_job_poller_graceful, GracefulPollOutcome}, }; @@ -52,33 +53,6 @@ use crate::{ lazy_static::lazy_static! { static ref TOOL_NAME_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_]+$").unwrap(); - /// Parse AI_HTTP_HEADERS environment variable into a vector of (header_name, header_value) tuples - /// Format: "header1: value1, header2: value2" - static ref AI_HTTP_HEADERS: Vec<(String, String)> = { - std::env::var("AI_HTTP_HEADERS") - .ok() - .map(|headers_str| { - headers_str - .split(',') - .filter_map(|header| { - let parts: Vec<&str> = header.splitn(2, ':').collect(); - if parts.len() == 2 { - let name = parts[0].trim().to_string(); - let value = parts[1].trim().to_string(); - if !name.is_empty() && !value.is_empty() { - Some((name, value)) - } else { - None - } - } else { - None - } - }) - .collect() - }) - .unwrap_or_default() - }; - static ref AI_AGENT_TOOL_SCHEMA: Box = to_raw_value(&serde_json::json!({ "type": "object", "properties": { @@ -791,7 +765,7 @@ pub async fn run_agent( let mut actions = vec![]; let mut content = None; - let mut final_usage: Option = None; + let mut final_usage: Option = None; // Check if this provider supports tools with the current output type let supports_tools = query_builder.supports_tools_with_output_type(output_type); @@ -893,7 +867,7 @@ pub async fn run_agent( .get_region() .unwrap_or(windmill_ai::ai_providers::USE_ENV_REGION); // Use Bedrock SDK via dedicated query builder - crate::ai::providers::bedrock::BedrockQueryBuilder::default() + windmill_ai::providers::bedrock::BedrockQueryBuilder::default() .execute_request( &messages, tool_defs.as_deref(), @@ -1231,7 +1205,8 @@ pub async fn run_agent( } ParsedResponse::Image { base64_data } => { // For image output, upload to S3 and track in conversation - let s3_object = upload_image_to_s3(&base64_data, job, client).await?; + let s3_object = + upload_image_to_s3(&base64_data, &job.workspace_id, &job.id, client).await?; let content = to_raw_value(&s3_object); diff --git a/backend/windmill-worker/src/ansible_executor.rs b/backend/windmill-worker/src/ansible_executor.rs index c8289b6934..2608cc6088 100644 --- a/backend/windmill-worker/src/ansible_executor.rs +++ b/backend/windmill-worker/src/ansible_executor.rs @@ -121,18 +121,24 @@ fn validate_relative_path(path: &str, field_name: &str) -> error::Result<()> { ))); } let p = std::path::Path::new(trimmed); - if p.is_absolute() { - return Err(error::Error::BadRequest(format!( - "`{}` must be a relative path inside the cloned repo, got: {}", - field_name, trimmed - ))); - } for component in p.components() { - if matches!(component, std::path::Component::ParentDir) { - return Err(error::Error::BadRequest(format!( - "`{}` must not contain `..` segments, got: {}", - field_name, trimmed - ))); + match component { + // RootDir catches leading `/` or `\`; Prefix catches Windows drive + // letters and UNC paths. `Path::is_absolute()` alone misses + // RootDir-only paths on Windows (e.g. `/etc/passwd`). + std::path::Component::RootDir | std::path::Component::Prefix(_) => { + return Err(error::Error::BadRequest(format!( + "`{}` must be a relative path inside the cloned repo, got: {}", + field_name, trimmed + ))); + } + std::path::Component::ParentDir => { + return Err(error::Error::BadRequest(format!( + "`{}` must not contain `..` segments, got: {}", + field_name, trimmed + ))); + } + _ => {} } } Ok(()) @@ -1765,9 +1771,19 @@ mod tests { #[test] fn test_validate_relative_path_rejects_absolute() { + // `/etc/passwd` isn't `is_absolute()` on Windows (no drive prefix), but + // its leading RootDir still escapes the cloned repo, so reject it on + // every platform. assert!(validate_relative_path("/etc/passwd", "playbook").is_err()); } + #[cfg(windows)] + #[test] + fn test_validate_relative_path_rejects_windows_absolute() { + assert!(validate_relative_path("\\etc\\passwd", "playbook").is_err()); + assert!(validate_relative_path("C:\\Windows\\System32", "playbook").is_err()); + } + #[test] fn test_validate_relative_path_rejects_parent_dir() { assert!(validate_relative_path("../escape.yml", "playbook").is_err()); diff --git a/backend/windmill-worker/src/common.rs b/backend/windmill-worker/src/common.rs index 0946075443..99e323dcfd 100644 --- a/backend/windmill-worker/src/common.rs +++ b/backend/windmill-worker/src/common.rs @@ -148,6 +148,13 @@ lazy_static::lazy_static! { static ref RE_RES_VAR: Regex = Regex::new(r#"\$(?:var|jsonvar|res|encrypted)\:"#).unwrap(); } +/// Returns true if any value in `vs` contains a `$var:`/`$jsonvar:`/`$res:`/`$encrypted:` +/// reference that would need interpolation by `transform_json`. Cheap pre-check that +/// callers can use to skip the DB roundtrip + clone path when nothing requires resolution. +pub(crate) fn map_needs_resolution(vs: &HashMap>) -> bool { + vs.values().any(|v| (*RE_RES_VAR).is_match(v.get())) +} + pub async fn transform_json<'a>( client: &AuthedClient, workspace: &str, diff --git a/backend/windmill-worker/src/global_cache.rs b/backend/windmill-worker/src/global_cache.rs index 23375746fd..67a8484b82 100644 --- a/backend/windmill-worker/src/global_cache.rs +++ b/backend/windmill-worker/src/global_cache.rs @@ -40,6 +40,9 @@ pub async fn build_tar_and_push( let tar_file = std::fs::File::create(&tar_path)?; let mut tar = tar::Builder::new(tar_file); tar.append_dir_all(".", &folder)?; + // Write the trailing zero blocks and close the inner file BEFORE std::fs::read + // below. Without this, the bytes we upload to S3 are an unfinalized archive. + drop(tar.into_inner()?); let tar_metadata = tokio::fs::metadata(&tar_path).await; if tar_metadata.is_err() || tar_metadata.as_ref().unwrap().len() == 0 { @@ -231,6 +234,7 @@ pub async fn save_cache( let tar_file = std::fs::File::create(&tar_path)?; let mut tar = tar::Builder::new(tar_file); tar.append_dir_all(".", &origin)?; + drop(tar.into_inner()?); let tar_metadata = tokio::fs::metadata(&tar_path).await; if tar_metadata.is_err() || tar_metadata.as_ref().unwrap().len() == 0 { tracing::info!("Failed to tar cache: {origin}"); diff --git a/backend/windmill-worker/src/memory_common.rs b/backend/windmill-worker/src/memory_common.rs index 440199702f..e024acea69 100644 --- a/backend/windmill-worker/src/memory_common.rs +++ b/backend/windmill-worker/src/memory_common.rs @@ -1,5 +1,5 @@ -use crate::ai::types::OpenAIMessage; use uuid::Uuid; +use windmill_ai::types::OpenAIMessage; use windmill_common::{db::DB, error::Error}; pub const MAX_MEMORY_SIZE_BYTES: usize = 100_000; // 100KB per memory entry in database diff --git a/backend/windmill-worker/src/memory_oss.rs b/backend/windmill-worker/src/memory_oss.rs index 74771a79e8..dcdab14e2a 100644 --- a/backend/windmill-worker/src/memory_oss.rs +++ b/backend/windmill-worker/src/memory_oss.rs @@ -3,7 +3,9 @@ pub use crate::memory_ee::*; #[cfg(not(all(feature = "private", feature = "enterprise")))] -use {crate::ai::types::OpenAIMessage, crate::memory_common, uuid::Uuid, windmill_common::db::DB}; +use { + crate::memory_common, uuid::Uuid, windmill_ai::types::OpenAIMessage, windmill_common::db::DB, +}; /// Read AI agent memory from storage /// In OSS: always reads from database diff --git a/backend/windmill-worker/src/python_executor.rs b/backend/windmill-worker/src/python_executor.rs index 5fb1278976..4db7b20fd5 100644 --- a/backend/windmill-worker/src/python_executor.rs +++ b/backend/windmill-worker/src/python_executor.rs @@ -2113,6 +2113,84 @@ async fn spawn_uv_install( } } +/// Verify that every file listed in the wheel's RECORD exists on disk under +/// `venv_p`. Used as a structural integrity check after both a successful +/// `pull_from_tar` (object-store cache hit) and a successful local +/// `uv pip install`, so a truncated tar or a dropped wheel entry can never +/// become an authoritative cache entry. Returns Err with a short description +/// on the first integrity issue (no .dist-info, no RECORD, or any listed +/// path missing on disk). +async fn verify_wheel_record(venv_p: &str) -> Result<(), String> { + let mut entries = tokio::fs::read_dir(venv_p) + .await + .map_err(|e| format!("read_dir({venv_p}): {e}"))?; + + let mut dist_info: Option = None; + loop { + match entries.next_entry().await { + Ok(Some(entry)) => { + let name = entry.file_name(); + let name_s = name.to_string_lossy(); + if name_s.ends_with(".dist-info") { + if let Ok(ft) = entry.file_type().await { + if ft.is_dir() { + dist_info = Some(name_s.into_owned()); + break; + } + } + } + } + Ok(None) => break, + Err(e) => return Err(format!("read_dir entry in {venv_p}: {e}")), + } + } + + let dist_info = match dist_info { + Some(d) => d, + None => return Err(format!("no .dist-info directory in {venv_p}")), + }; + + let record_path = format!("{venv_p}/{dist_info}/RECORD"); + let record_content = tokio::fs::read_to_string(&record_path) + .await + .map_err(|e| format!("read RECORD at {record_path}: {e}"))?; + + let mut missing: Vec = Vec::new(); + for line in record_content.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + let rel_path = match trimmed.split(',').next() { + Some(p) if !p.is_empty() => p, + _ => continue, + }; + // Defensive: skip absolute paths or escaping entries — we only + // validate package-relative files. + if rel_path.starts_with('/') || rel_path.contains("..") { + continue; + } + let full = format!("{venv_p}/{rel_path}"); + if tokio::fs::metadata(&full).await.is_err() { + missing.push(rel_path.to_string()); + // Bound error size in pathological cases (e.g. wholly empty dir). + if missing.len() >= 10 { + missing.push("...".to_string()); + break; + } + } + } + + if missing.is_empty() { + Ok(()) + } else { + Err(format!( + "wheel RECORD lists files missing on disk: {}", + missing.join(", ") + )) + } +} + /// uv pip install, include cached or pull from S3 pub async fn handle_python_reqs( requirements: Vec, @@ -2484,6 +2562,30 @@ pub async fn handle_python_reqs( workspace_id = %w_id, "No tarball was found for {venv_p} on S3 or different problem occurred {job_id}:\n{e}", ); + } else if let Err(verify_err) = verify_wheel_record(&venv_p).await { + // The object-store tar extracted cleanly but the resulting + // directory is missing files referenced by the wheel RECORD. + // Wipe the broken cache entry and fall through to a fresh + // local install rather than treating it as authoritative. + tracing::warn!( + workspace_id = %w_id, + job_id = %job_id, + "Object-store cache for {venv_p} failed wheel RECORD verification, will reinstall locally: {verify_err}" + ); + if let Err(rm_err) = tokio::fs::remove_dir_all(&venv_p).await { + tracing::warn!( + workspace_id = %w_id, + "could not remove broken cache dir {venv_p}: {rm_err}" + ); + } + append_logs( + &job_id, + &w_id, + format!( + "\n[!] cached wheel for {req} from object store failed integrity check, reinstalling: {verify_err}\n" + ), + &conn, + ).await; } else { print_success( true, @@ -2640,6 +2742,39 @@ pub async fn handle_python_reqs( let _ = std::fs::remove_file(format!("{job_dir}/{req}.config.proto")); } + // Verify the install before declaring success: if uv exited 0 but + // the on-disk directory is missing files the wheel RECORD says + // should exist, do NOT write .valid.windmill, do NOT queue the + // piptar upload, and fail the job. This prevents a broken tar + // from ever being pushed to the object store and propagated to + // every other replica. + if let Err(verify_err) = verify_wheel_record(&venv_p).await { + tracing::error!( + workspace_id = %w_id, + job_id = %job_id, + "uv pip install of {req} into {venv_p} failed wheel RECORD verification: {verify_err}" + ); + append_logs( + &job_id, + &w_id, + format!( + "\nWheel RECORD verification failed after install of {req}: {verify_err}. \ + Aborting to avoid publishing a corrupt cache entry." + ), + &conn, + ).await; + if let Err(rm_err) = tokio::fs::remove_dir_all(&venv_p).await { + tracing::warn!( + workspace_id = %w_id, + "could not remove broken install dir {venv_p}: {rm_err}" + ); + } + pids.lock().await.get_mut(i).and_then(|e| e.take()); + return Err(Error::from(anyhow!( + "wheel RECORD verification failed after install of {req}" + ))); + } + print_success( false, s3_push, diff --git a/backend/windmill-worker/src/result_processor.rs b/backend/windmill-worker/src/result_processor.rs index 1f771f6879..9eac0a5dcd 100644 --- a/backend/windmill-worker/src/result_processor.rs +++ b/backend/windmill-worker/src/result_processor.rs @@ -35,8 +35,9 @@ use windmill_common::{ use windmill_common::bench::{BenchmarkInfo, BenchmarkIter}; use windmill_queue::{ - append_logs, get_mini_completed_job, CanceledBy, FlowRunners, JobCompleted, MiniCompletedJob, - MiniPulledJob, ValidableJson, WrappedError, INIT_SCRIPT_TAG, + append_logs, get_mini_completed_job, is_pre_shaped_wm_failure_result, CanceledBy, FlowRunners, + JobCompleted, MiniCompletedJob, MiniPulledJob, ValidableJson, WrappedError, INIT_SCRIPT_TAG, + MANUAL_FAILURE_ERROR_NAME, }; use serde_json::{json, value::RawValue, Value}; @@ -61,8 +62,37 @@ struct ErrorMessage { name: String, } +#[derive(Debug, Deserialize)] +struct NestedErrorMessage { + error: ErrorMessage, +} + +/// Extract `{ name, message }` from a result. Accepts both the standard +/// top-level shape (regular runtime errors) and the nested `{ error: { name, +/// message }, ... }` shape produced by the wm_failure injection. +/// +/// For wm_failure-injected results, we prefer the nested error: a successful +/// run may legitimately contain top-level `name`/`message` fields (user data +/// named `name`/`message`), and we want OTel to record the ManualFailure +/// rather than the user's sibling fields. +fn extract_error_message(raw: &str) -> Option { + let nested = serde_json::from_str::(raw) + .ok() + .map(|n| n.error); + if matches!(&nested, Some(em) if em.name == MANUAL_FAILURE_ERROR_NAME) { + return nested; + } + if let Ok(em) = serde_json::from_str::(raw) { + return Some(em); + } + nested +} + +/// Returns the post-processing `success` value (after any `wm_failure` +/// override). Callers use this to make worker-loop decisions that depend on +/// whether the job ultimately succeeded — e.g. the init-script killpill. async fn process_jc( - jc: JobCompleted, + mut jc: JobCompleted, worker_name: &str, base_internal_url: &str, db: &DB, @@ -73,7 +103,32 @@ async fn process_jc( killpill_rx: &tokio::sync::broadcast::Receiver<()>, #[cfg(feature = "benchmark")] bench: &mut BenchmarkIter, #[cfg(feature = "benchmark")] bench_infos: &mut BenchmarkInfo, -) { +) -> bool { + // Parse `wm_labels` and `wm_failure` together (single `from_str`) + // so we don't deserialize the whole result twice on every job. + let metadata = jc.result.result_metadata(); + + // If the script returned a `wm_failure: ` field in its + // result, tag the run as a failure. Inject an `error: { name, message }` + // at the top level so error handlers / UI / OTel see the standard error + // shape, while preserving sibling fields (`windmill_status_code`, + // `windmill_content_type`, `windmill_headers`, the user's data) at the + // top level so sync webhook responses still honor them. + if jc.success { + if let Some(failure_msg) = metadata.wm_failure.as_ref() { + if let Ok(Value::Object(mut map)) = serde_json::from_str::(jc.result.get()) { + map.insert( + "error".to_string(), + json!({ "name": MANUAL_FAILURE_ERROR_NAME, "message": failure_msg }), + ); + if let Ok(raw) = serde_json::value::to_raw_value(&Value::Object(map)) { + jc.result = Arc::new(raw); + } + } + jc.success = false; + } + } + let success: bool = jc.success; let span = if success { @@ -125,7 +180,7 @@ async fn process_jc( jc.job.id }; - if let Some(labels) = jc.result.wm_labels() { + if let Some(labels) = metadata.wm_labels.as_ref() { if !labels.is_empty() { span.record("labels", labels.join(",")); } @@ -163,7 +218,7 @@ async fn process_jc( span.record("script_hash", script_hash.to_string().as_str()); } if !success { - if let Ok(result_error) = serde_json::from_str::(jc.result.get()) { + if let Some(result_error) = extract_error_message(jc.result.get()) { span.record("error.message", result_error.message.as_str()); span.record("error.name", result_error.name.as_str()); span.record( @@ -218,6 +273,8 @@ async fn process_jc( ) .await; } + + success } enum JobCompletedRx { @@ -311,8 +368,7 @@ pub fn start_background_processor( result: SendResultPayload::JobCompleted(jc), time, }) => { - let is_init_script_and_failure = - !jc.success && jc.job.tag.as_str() == INIT_SCRIPT_TAG; + let is_init_script = jc.job.tag.as_str() == INIT_SCRIPT_TAG; let is_dependency_job = matches!( jc.job.kind, JobKind::Dependencies | JobKind::FlowDependencies @@ -322,7 +378,10 @@ pub fn start_background_processor( #[cfg(feature = "benchmark")] let is_top_level_job = jc.job.parent_job.is_none(); - process_jc( + // process_jc returns the post-override success value so a + // job that flipped to failure via `wm_failure` still + // triggers the init-script killpill. + let final_success = process_jc( jc, &worker_name, &base_internal_url, @@ -340,7 +399,7 @@ pub fn start_background_processor( .warn_after_seconds(10) .await; - if is_init_script_and_failure { + if is_init_script && !final_success { tracing::error!("init script errored, exiting"); killpill_tx.send(); break; @@ -793,19 +852,44 @@ pub async fn process_completed_job( } } } else { - let result = add_completed_job_error( - db, - &job, - mem_peak.to_owned(), - canceled_by.clone(), - serde_json::from_str(result.get()).unwrap_or_else( - |_| json!({ "message": format!("Non serializable error: {}", result.get()) }), - ), - worker_name, - false, - None, - ) - .await?; + // The result already carries our injected + // `error: { name: "ManualFailure", ... }` marker when process_jc + // retagged a successful run as a failure — store it as-is to preserve + // sibling fields like `windmill_status_code`. We check for the + // injected marker specifically (not just the presence of a + // `wm_failure` field) so a real runtime failure whose raw + // result happens to contain a `wm_failure` field still goes + // through the standard `WrappedError { error: ... }` wrap path. + let downstream_result: Arc> = if is_pre_shaped_wm_failure_result(result.get()) + { + windmill_queue::add_completed_job_pre_shaped_failure( + db, + &job, + mem_peak.to_owned(), + canceled_by.clone(), + Json(&*result), + worker_name, + false, + None, + ) + .await?; + result.clone() + } else { + let wrapped = add_completed_job_error( + db, + &job, + mem_peak.to_owned(), + canceled_by.clone(), + serde_json::from_str(result.get()).unwrap_or_else( + |_| json!({ "message": format!("Non serializable error: {}", result.get()) }), + ), + worker_name, + false, + None, + ) + .await?; + Arc::new(serde_json::value::to_raw_value(&wrapped).unwrap()) + }; if job.is_flow_step() { if let Some(parent_job) = job.parent_job { tracing::error!(parent_flow = %parent_job, subflow = %job.id, "process completed job error, updating flow status"); @@ -817,7 +901,7 @@ pub async fn process_completed_job( &job.workspace_id, false, canceled_by, - Arc::new(serde_json::value::to_raw_value(&result).unwrap()), + downstream_result, duration.and_then(|d| { job.started_at.map(|started_at| FlowJobDuration { started_at: started_at, @@ -855,13 +939,12 @@ pub async fn process_completed_job( .fetch_optional(db) .await?; if let Some(Some(job_ids)) = job_ids_json { - let err_result = Arc::new(serde_json::value::to_raw_value(&result).unwrap()); if let Ok(Some(_)) = handle_wac_child_completion( db, &job.id, parent_job, &job.workspace_id, - err_result, + downstream_result, false, job_ids, ) diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index 050ae4b44f..53c94a7458 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -81,6 +81,33 @@ use windmill_audit::ActionKind; use windmill_common::audit::AuditAuthor; use windmill_queue::{canceled_job_to_result, push}; +lazy_static::lazy_static! { + /// Per-worker LRU cache of resolved `flow_env` values, keyed by flow job id. + /// `update_flow_status_after_job_completion_internal` runs once per child-step + /// completion, and re-resolving `$var:`/`$res:` references each time was the + /// dominant heap-allocation source under flow-heavy load. The cache aligns + /// predicate evaluation with `handle_flow`'s input-transform path, which + /// already resolves once at flow entry — predicates were the only place still + /// re-resolving on every read. + /// + /// Per-worker scope (caveat): the cache lives in process memory, not the DB, + /// so different workers processing children of the same flow each compute + /// their own snapshot on first miss. If a `$var:`/`$res:` value mutates + /// mid-flow, predicate eval on different workers can observe different + /// values for the same flow run. For typical use (env values configured at + /// flow start, read-only during execution) this is invisible. Cross-worker + /// determinism would require persisting the resolved env in `v2_job_status`; + /// see follow-up notes. + /// + /// Entries become dead weight once a flow completes and are evicted by LRU + /// pressure. Bounded at 1024 entries; per-entry footprint depends on the + /// env's contents (literals are small but a single resolved `$res:` can be + /// tens of KB), so worst-case memory scales with workload mix rather than + /// being fixed. + static ref RESOLVED_FLOW_ENV_CACHE: quick_cache::sync::Cache>>> = + quick_cache::sync::Cache::new(1024); +} + #[derive(Debug)] pub struct SchedulePushZombieError(pub String); @@ -467,21 +494,54 @@ pub async fn update_flow_status_after_job_completion_internal( } // Resolve flow_env for predicate evaluations (stop_after_if, - // stop_after_all_iters_if, retry_if). Only fetch when one of these - // predicates is configured to avoid an extra DB query on the common - // path. `retry` without `retry_if` doesn't consult flow_env. - let retry_uses_flow_env = - |module: &FlowModule| module.retry.as_ref().is_some_and(|r| r.retry_if.is_some()); + // stop_after_all_iters_if, retry_if). Two-level gate: + // 1. Structural — at least one such predicate is configured. + // 2. Textual — the predicate expression actually references + // `flow_env`. Mirrors the existing `expr.contains("results.")` + // check in `get_id_ctx_for_expr`. A substring match has false + // positives (harmless — just runs an unneeded resolve) but no + // false negatives, since `flow_env` must appear textually for the + // expression engine to read it. + let expr_uses_flow_env = |expr: &str| expr.contains("flow_env"); + let retry_if_uses_flow_env = |module: &FlowModule| { + module + .retry + .as_ref() + .and_then(|r| r.retry_if.as_ref()) + .is_some_and(|r| expr_uses_flow_env(&r.expr)) + }; let needs_flow_env = current_module.is_some_and(|m| { - m.stop_after_if.is_some() - || m.stop_after_all_iters_if.is_some() - || retry_uses_flow_env(m) + m.stop_after_if + .as_ref() + .is_some_and(|s| expr_uses_flow_env(&s.expr)) + || m.stop_after_all_iters_if + .as_ref() + .is_some_and(|s| expr_uses_flow_env(&s.expr)) + || retry_if_uses_flow_env(m) }) || flow_value .failure_module .as_ref() - .is_some_and(|fm| retry_uses_flow_env(fm)); - let resolved_flow_env: Option>> = if needs_flow_env { - resolve_flow_env_for_status_update(db, client, flow, w_id, flow_value).await + .is_some_and(|fm| retry_if_uses_flow_env(fm)); + // The resolved env is constant for a flow's lifetime, so cache it by flow + // job id and reuse across child-step completions. Cache miss falls back to + // the existing resolve+persist path; same-worker subsequent completions are + // a single Arc::clone away. Transient-failure fallbacks (`is_cacheable == + // false`) bypass the insert so a single API blip doesn't poison the rest + // of the flow run. + let resolved_flow_env: Option>>> = if needs_flow_env { + if let Some(cached) = RESOLVED_FLOW_ENV_CACHE.get(&flow) { + Some(cached) + } else { + resolve_flow_env_for_status_update(db, client, flow, w_id, flow_value) + .await + .map(|(env, is_cacheable)| { + let arc = Arc::new(env); + if is_cacheable { + RESOLVED_FLOW_ENV_CACHE.insert(flow, arc.clone()); + } + arc + }) + } } else { None }; @@ -632,7 +692,7 @@ pub async fn update_flow_status_after_job_completion_internal( let bool_res = compute_bool_from_expr( &expr, Marc::new(args), - resolved_flow_env.as_ref(), + resolved_flow_env.as_deref(), result.clone(), all_iters, id_ctx.as_ref(), @@ -916,7 +976,7 @@ pub async fn update_flow_status_after_job_completion_internal( &mut stop_early_err_msg, &mut nresult, args, - resolved_flow_env.as_ref(), + resolved_flow_env.as_deref(), flow, &old_status, ) @@ -1136,7 +1196,7 @@ pub async fn update_flow_status_after_job_completion_internal( &mut stop_early_err_msg, &mut nresult, args, - resolved_flow_env.as_ref(), + resolved_flow_env.as_deref(), flow, &old_status, ) @@ -1219,7 +1279,7 @@ pub async fn update_flow_status_after_job_completion_internal( &old_status.retry, result.clone(), Marc::new(args), - resolved_flow_env.as_ref(), + resolved_flow_env.as_deref(), Some(client), ) .await? @@ -1601,7 +1661,7 @@ pub async fn update_flow_status_after_job_completion_internal( &old_status.retry, result.clone(), Marc::new(args), - resolved_flow_env.as_ref(), + resolved_flow_env.as_deref(), Some(client), ) .await? @@ -2387,28 +2447,62 @@ async fn fetch_root_flow_id(db: &DB, flow_id: Uuid) -> Uuid { // `update_flow_status_after_job_completion_internal`: take the current flow's // `flow_env` if present, otherwise inherit from the root flow, then interpolate // any `$var:`/`$res:` references via `transform_json`. +// +// Returns `Some((env, is_cacheable))`. `is_cacheable` is `false` only when the +// returned env is a partially-resolved fallback after a transient error (e.g. +// `transform_json` failed mid-resolve, or the mini job fetch failed). Callers +// must not cache `is_cacheable == false` results — doing so would freeze the +// transient failure for the rest of the flow run. async fn resolve_flow_env_for_status_update( db: &DB, client: &AuthedClient, flow_job_id: Uuid, workspace_id: &str, flow_value: &FlowValue, -) -> Option>> { - let env = if let Some(ref e) = flow_value.flow_env { - e.clone() - } else { - fetch_root_flow_env(db, flow_job_id, workspace_id).await? - }; +) -> Option<(HashMap>, bool)> { + // Fetch the env source. For the inherited path, we first need to know whether the + // flow even has a parent — `fetch_root_flow_env` runs a recursive CTE on `v2_job` + // and is wasted work for top-level flows with no own `flow_env`. The mini job we + // pull here can be reused below if `transform_json` ends up needing it. + let (env, mini): (HashMap>, Option) = + if let Some(ref e) = flow_value.flow_env { + (e.clone(), None) + } else { + let mini = match get_mini_pulled_job(db, &flow_job_id).await { + Ok(Some(j)) => j, + Ok(None) => return None, + Err(e) => { + tracing::warn!("Failed to fetch flow job to resolve flow_env: {e:#}"); + return None; + } + }; + if mini.parent_job.is_none() { + // No own flow_env and no parent to inherit from — nothing to resolve. + return None; + } + let env = fetch_root_flow_env(db, flow_job_id, workspace_id).await?; + (env, Some(mini)) + }; if env.is_empty() { - return Some(env); + return Some((env, true)); } - let mini = match get_mini_pulled_job(db, &flow_job_id).await { - Ok(Some(j)) => j, - Ok(None) => return Some(env), - Err(e) => { - tracing::warn!("Failed to fetch flow job to resolve flow_env: {e:#}"); - return Some(env); - } + // Skip the DB roundtrip + `transform_json` when nothing in env needs interpolation. + // This is the common case: a flow_env containing only literal values. + if !crate::common::map_needs_resolution(&env) { + return Some((env, true)); + } + let mini = match mini { + Some(m) => m, + None => match get_mini_pulled_job(db, &flow_job_id).await { + Ok(Some(j)) => j, + // Don't cache: we couldn't fetch the job, so the env we'd return is the + // pre-interpolation literal — caching that would freeze the broken state. + Ok(None) => return Some((env, false)), + Err(e) => { + tracing::warn!("Failed to fetch flow job to resolve flow_env: {e:#}"); + return Some((env, false)); + } + }, }; match transform_json( client, @@ -2419,11 +2513,13 @@ async fn resolve_flow_env_for_status_update( ) .await { - Ok(Some(resolved)) => Some(resolved), - Ok(None) => Some(env), + Ok(Some(resolved)) => Some((resolved, true)), + Ok(None) => Some((env, true)), + // Don't cache transient resolution failures: one variable/resource API blip + // would otherwise poison the literal-only env for the rest of the flow run. Err(e) => { tracing::warn!("Failed to resolve flow_env references in status update: {e:#}"); - Some(env) + Some((env, false)) } } } @@ -2648,8 +2744,11 @@ pub async fn handle_flow( // Resolve $var: and $res: references in flow_env. // We resolve into a separate variable to avoid cloning the entire FlowValue // (which includes modules, failure_module, etc.) just to replace flow_env. + // `is_cacheable` is `false` only on a transient `transform_json` failure, + // matching `resolve_flow_env_for_status_update`'s contract — we must not + // freeze a partial resolution into the cache. let resolved_env; - let flow_env = if let Some(env) = env_source { + let (flow_env, is_cacheable) = if let Some(env) = env_source { match transform_json( client, &flow_job.workspace_id, @@ -2661,18 +2760,31 @@ pub async fn handle_flow( { Ok(Some(resolved)) => { resolved_env = resolved; - Some(&resolved_env) + (Some(&resolved_env), true) } - Ok(None) => Some(env), + Ok(None) => (Some(env), true), Err(e) => { tracing::warn!("Failed to resolve flow_env references: {e}"); - Some(env) + (Some(env), false) } } } else { - None + (None, true) }; + // Populate the per-flow resolved-env cache so subsequent predicate + // evaluations in `update_flow_status_after_job_completion_internal` skip + // the recursive CTE + transform_json. Costs one HashMap clone per + // sub-flow handle_flow entry; saves up to one CTE + one transform_json + // per flow execution that has predicates referencing flow_env. + if is_cacheable { + if let Some(env) = flow_env { + if !env.is_empty() { + RESOLVED_FLOW_ENV_CACHE.insert(flow_job.id, Arc::new(env.clone())); + } + } + } + let status = flow_job .parse_flow_status() .with_context(|| "Unable to parse flow status")?; diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index 2e1f9ef06a..22cc1f75eb 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.697.0"; +export const VERSION = "v1.700.2"; export async function login(email: string, password: string): Promise { return await windmill.UserService.login({ diff --git a/cli/src/commands/app/app.ts b/cli/src/commands/app/app.ts index f99df84636..2749c0269c 100644 --- a/cli/src/commands/app/app.ts +++ b/cli/src/commands/app/app.ts @@ -4,7 +4,7 @@ import { Command } from "@cliffy/command"; import { Table } from "@cliffy/table"; import { colors } from "@cliffy/ansi/colors"; import * as log from "../../core/log.ts"; -import { sep as SEP } from "node:path"; +import { sep as SEP, isAbsolute, resolve as pathResolve, relative as pathRelative, basename } from "node:path"; import { stat } from "node:fs/promises"; import * as windmillUtils from "@windmill-labs/shared-utils"; import { yamlParseFile } from "../../utils/yaml.ts"; @@ -12,6 +12,7 @@ import * as wmill from "../../../gen/services.gen.ts"; import { ListableApp, Policy } from "../../../gen/types.gen.ts"; import { GlobalOptions, isSuperset } from "../../types.ts"; +import { getWmillYamlPath } from "../../core/conf.ts"; import { readInlinePathSync } from "../../utils/utils.ts"; import devCommand from "./dev.ts"; import lintCommand from "./lint.ts"; @@ -261,31 +262,122 @@ async function get(opts: GlobalOptions & { json?: boolean }, path: string) { } } -async function push(opts: GlobalOptions, filePath: string, remotePath: string) { - if (!validatePath(remotePath)) { - return; - } +const APP_FOLDER_SUFFIXES = ["__raw_app", ".raw_app", "__app", ".app"] as const; + +async function push( + opts: GlobalOptions, + filePath?: string, + remotePath?: string +) { + // Capture original CWD before resolveWorkspace, which may chdir to the + // wmill.yaml root. We need it to resolve relative inputs and to derive + // the remote path from the user's location when auto-inferring. + const originalCwd = process.cwd(); + const workspace = await resolveWorkspace(opts); await requireLogin(opts); - // Detect raw apps by checking for raw_app.yaml or __raw_app/.raw_app suffix - const normalizedPath = filePath.endsWith(SEP) ? filePath.slice(0, -1) : filePath; - const isRawApp = normalizedPath.endsWith("__raw_app") || normalizedPath.endsWith(".raw_app"); + // Auto-infer file path from CWD when omitted + if (!filePath) { + filePath = originalCwd; + } + const absoluteFilePath = isAbsolute(filePath) + ? filePath + : pathResolve(originalCwd, filePath); + + // Detect app folder type (regular vs raw) + const normalizedPath = absoluteFilePath.endsWith(SEP) + ? absoluteFilePath.slice(0, -1) + : absoluteFilePath; + const dirName = basename(normalizedPath); + const isRawAppByName = + dirName.endsWith("__raw_app") || dirName.endsWith(".raw_app"); + const isAppByName = dirName.endsWith("__app") || dirName.endsWith(".app"); + let hasRawAppYaml = false; - if (!isRawApp) { + let hasAppYaml = false; + try { + await stat(normalizedPath + SEP + "raw_app.yaml"); + hasRawAppYaml = true; + } catch { /* not a raw app */ } + if (!hasRawAppYaml) { try { - const rawAppPath = (filePath.endsWith(SEP) ? filePath : filePath + SEP) + "raw_app.yaml"; - await stat(rawAppPath); - hasRawAppYaml = true; - } catch { /* not a raw app */ } + await stat(normalizedPath + SEP + "app.yaml"); + hasAppYaml = true; + } catch { /* not an app */ } } - if (isRawApp || hasRawAppYaml) { + if (!isRawAppByName && !isAppByName && !hasRawAppYaml && !hasAppYaml) { + log.error( + colors.red( + `'${filePath}' is not an app folder (no app.yaml or raw_app.yaml, and not a *.app/*.raw_app folder).` + ) + ); + return; + } + + // Auto-infer remote path from the folder location relative to wmill.yaml root + if (!remotePath) { + const wmillYamlPath = getWmillYamlPath(); + if (!wmillYamlPath) { + log.error( + colors.red( + "Could not infer remote path: no wmill.yaml found. Run 'wmill init' or pass explicitly." + ) + ); + return; + } + // After resolveWorkspace, process.cwd() is the wmill.yaml dir + const wmillRoot = process.cwd(); + let inferred = pathRelative(wmillRoot, normalizedPath).replaceAll(SEP, "/"); + if (inferred.startsWith("..") || isAbsolute(inferred)) { + log.error( + colors.red( + `Could not infer remote path: '${filePath}' is outside the wmill.yaml root (${wmillRoot}). Move the folder under the root or pass explicitly.` + ) + ); + return; + } + for (const suffix of APP_FOLDER_SUFFIXES) { + if (inferred.endsWith(suffix)) { + inferred = inferred.slice(0, -suffix.length); + break; + } + } + if (!inferred) { + log.error( + colors.red( + "Could not infer remote path: app folder is at the wmill.yaml root. Pass explicitly." + ) + ); + return; + } + if ( + !inferred.startsWith("u/") && + !inferred.startsWith("g/") && + !inferred.startsWith("f/") + ) { + log.error( + colors.red( + `Could not infer remote path: '${inferred}' is not under u/, g/, or f/. Move the app under one of these prefixes or pass explicitly.` + ) + ); + return; + } + remotePath = inferred; + log.info(colors.gray(`Inferred remote path: ${remotePath}`)); + } + + if (!validatePath(remotePath)) { + return; + } + + if (isRawAppByName || hasRawAppYaml) { const { pushRawApp } = await import("./raw_apps.ts"); - await pushRawApp(workspace.workspaceId, remotePath, filePath); + await pushRawApp(workspace.workspaceId, remotePath, absoluteFilePath); log.info(colors.bold.underline.green("Raw app pushed")); } else { - await pushApp(workspace.workspaceId, remotePath, filePath); + await pushApp(workspace.workspaceId, remotePath, absoluteFilePath); log.info(colors.bold.underline.green("App pushed")); } } @@ -301,8 +393,11 @@ const command = new Command() .arguments("") .option("--json", "Output as JSON (for piping to jq)") .action(get as any) - .command("push", "push a local app ") - .arguments(" ") + .command( + "push", + "push a local app. With no args, infers the app from the current directory and the remote path from its location relative to wmill.yaml." + ) + .arguments("[file_path:string] [remote_path:string]") .action(push as any) .command("dev", devCommand) .command("lint", lintCommand) diff --git a/cli/src/commands/app/dev.ts b/cli/src/commands/app/dev.ts index 5b78f84e3f..7edb793bea 100644 --- a/cli/src/commands/app/dev.ts +++ b/cli/src/commands/app/dev.ts @@ -14,7 +14,8 @@ import * as path from "node:path"; import process from "node:process"; import { Buffer } from "node:buffer"; import { writeFileSync } from "node:fs"; -import { readTextFile } from "../../utils/utils.ts"; +import { getHeaders, readTextFile } from "../../utils/utils.ts"; +import { detectAuthGatewayChallenge } from "../../utils/http_guards.ts"; import { WebSocket, WebSocketServer } from "ws"; import { createFrameworkPlugins, @@ -1714,13 +1715,17 @@ async function streamJobWithSSE( const sseUrl = `${baseUrl}api/w/${workspace}/jobs_u/getupdate_sse/${jobId}?fast=true`; + const extraHeaders = getHeaders(); const response = await fetch(sseUrl, { headers: { Accept: "text/event-stream", Authorization: `Bearer ${token}`, + ...extraHeaders, }, }); + await detectAuthGatewayChallenge(response, sseUrl); + if (!response.ok) { throw new Error( `SSE request failed: ${response.status} ${response.statusText}`, diff --git a/cli/src/commands/app/new.ts b/cli/src/commands/app/new.ts index 2072c0312c..87d40f161b 100644 --- a/cli/src/commands/app/new.ts +++ b/cli/src/commands/app/new.ts @@ -168,7 +168,7 @@ const templates: Record = { "/index.css": indexCss, "/package.json": `{ "dependencies": { - "svelte": "5.45.2", + "svelte": "^5.55.5", "windmill-client": "^1" } }`, diff --git a/cli/src/commands/docs/docs.ts b/cli/src/commands/docs/docs.ts index d86d4ca367..fed91201e0 100644 --- a/cli/src/commands/docs/docs.ts +++ b/cli/src/commands/docs/docs.ts @@ -4,6 +4,8 @@ import * as log from "../../core/log.ts"; import { requireLogin } from "../../core/auth.ts"; import { resolveWorkspace } from "../../core/context.ts"; import { GlobalOptions } from "../../types.ts"; +import { getHeaders } from "../../utils/utils.ts"; +import { detectAuthGatewayChallenge } from "../../utils/http_guards.ts"; interface DocContentItem { title: string; @@ -36,6 +38,7 @@ async function docs( console.log(colors.bold(`\nSearching Windmill docs...\n`)); + const extraHeaders = getHeaders(); let res: Response; try { res = await fetch(url, { @@ -43,6 +46,7 @@ async function docs( headers: { "Content-Type": "application/json", Authorization: `Bearer ${workspace.token}`, + ...extraHeaders, }, body: JSON.stringify({ query }), }); @@ -50,6 +54,8 @@ async function docs( throw new Error(`Network error connecting to ${workspace.remote}: ${e}`); } + await detectAuthGatewayChallenge(res, url); + if (res.status === 403) { log.info( "Windmill documentation search is an Enterprise Edition feature. Please upgrade to use this command." diff --git a/cli/src/commands/generate-metadata/generate-metadata.ts b/cli/src/commands/generate-metadata/generate-metadata.ts index 226220592e..1de173e582 100644 --- a/cli/src/commands/generate-metadata/generate-metadata.ts +++ b/cli/src/commands/generate-metadata/generate-metadata.ts @@ -8,6 +8,8 @@ import { resolveWorkspace } from "../../core/context.ts"; import { requireLogin } from "../../core/auth.ts"; import * as log from "../../core/log.ts"; import { + beginLockfileBatch, + flushLockfileBatch, generateScriptMetadataInternal, getRawWorkspaceDependencies, readLockfile, @@ -200,6 +202,12 @@ export async function rehashOnly( const stubWorkspace = {} as any; const rehashOpts = { ...opts, rehashOnly: true } as any; + type RehashTask = + | { kind: "script"; scriptPath: string } + | { kind: "flow"; folder: string } + | { kind: "app"; folder: string; rawApp: boolean }; + const queue: RehashTask[] = []; + if (!rehashFilter?.skipScripts) { for (const e of scriptPaths) { // Filter against the derived remote path so a folder argument like @@ -210,14 +218,7 @@ export async function rehashOnly( if (rehashFilter?.missingOnly) { if (skipIfExisting(remotePath) || skipIfExisting(remotePath, "__script_hash")) continue; } - try { - await generateScriptMetadataInternal( - e, stubWorkspace, rehashOpts, false, true, {}, codebases, false, - ); - counts.scripts++; - } catch (err) { - log.warn(`Skipping ${e}: ${err instanceof Error ? err.message : err}`); - } + queue.push({ kind: "script", scriptPath: e }); } } @@ -228,12 +229,7 @@ export async function rehashOnly( const folderNormalized = f.replaceAll(SEP, "/"); if (skipIfExisting(folderNormalized, "__flow_hash")) continue; } - try { - await generateFlowLockInternal(f, false, stubWorkspace, rehashOpts, false, true); - counts.flows++; - } catch (err) { - log.warn(`Skipping ${f}: ${err instanceof Error ? err.message : err}`); - } + queue.push({ kind: "flow", folder: f }); } } @@ -244,13 +240,52 @@ export async function rehashOnly( const folderNormalized = appFolder.replaceAll(SEP, "/"); if (skipIfExisting(folderNormalized, "__app_hash")) continue; } - try { - await generateAppLocksInternal(appFolder, rawApp, false, stubWorkspace, rehashOpts, false, true); - counts.apps++; - } catch (err) { - log.warn(`Skipping ${appFolder}: ${err instanceof Error ? err.message : err}`); + queue.push({ kind: "app", folder: appFolder, rawApp }); + } + } + + let parallelism = Number(opts.parallel ?? 1); + if (!Number.isFinite(parallelism) || parallelism <= 0) parallelism = 1; + if (parallelism > 1) { + log.info(`Parallelizing ${parallelism} items at a time`); + } + + // Buffer wmill-lock.yaml writes during the parallel phase: each task mutates + // the shared in-memory lockfile, then we flush once. + await beginLockfileBatch(); + try { + const pool = new Set>(); + while (queue.length > 0 || pool.size > 0) { + while (pool.size < parallelism && queue.length > 0) { + const task = queue.shift()!; + const p = (async () => { + try { + if (task.kind === "script") { + await generateScriptMetadataInternal( + task.scriptPath, stubWorkspace, rehashOpts, false, true, {}, codebases, false, + ); + counts.scripts++; + } else if (task.kind === "flow") { + await generateFlowLockInternal(task.folder, false, stubWorkspace, rehashOpts, false, true); + counts.flows++; + } else { + await generateAppLocksInternal(task.folder, task.rawApp, false, stubWorkspace, rehashOpts, false, true); + counts.apps++; + } + } catch (err) { + const label = task.kind === "script" ? task.scriptPath : task.folder; + log.warn(`Skipping ${label}: ${err instanceof Error ? err.message : err}`); + } + })(); + pool.add(p); + p.then(() => pool.delete(p)); + } + if (pool.size > 0) { + await Promise.race(pool); } } + } finally { + await flushLockfileBatch(); } if (counts.scripts + counts.flows + counts.apps > 0 || !rehashFilter?.missingOnly) { @@ -519,85 +554,117 @@ export async function generateMetadata( const errors: { path: string; error: string }[] = []; - // Process scripts - for (const item of scripts) { - current++; - log.info(`${formatProgress(current)} script ${item.path}`); - try { - await generateScriptMetadataInternal( - item.path, // originalPath with extension - workspace, - opts, - false, // dryRun - true, // noStaleMessage - mismatchedWorkspaceDeps, - codebases, - false, - tree - ); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - errors.push({ path: item.path, error: msg }); - log.error(` Failed: ${msg}`); - } + let parallelism = Number(opts.parallel ?? 1); + if (!Number.isFinite(parallelism) || parallelism <= 0) parallelism = 1; + if (parallelism > 1) { + log.info(`Parallelizing ${parallelism} items at a time`); } - // Process flows - for (const item of flows) { - current++; - try { - const result = await generateFlowLockInternal( - item.folder.replaceAll("/", SEP), - false, // dryRun - workspace, - opts, - false, - true, // noStaleMessage - tree - ); - const flowResult = result as FlowLocksResult | undefined; - const scriptsInfo = flowResult?.updatedScripts?.length - ? colors.dim(colors.white(`: ${flowResult.updatedScripts.join(", ")}`)) - : ""; - log.info(`${formatProgress(current)} flow ${item.path}${scriptsInfo}`); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - errors.push({ path: item.path, error: msg }); - log.info(`${formatProgress(current)} flow ${item.path}`); - log.error(` Failed: ${msg}`); - } - } + type Task = + | { kind: "script"; item: StaleItem } + | { kind: "flow"; item: StaleItem } + | { kind: "app"; item: StaleItem }; + const queue: Task[] = [ + ...scripts.map((item) => ({ kind: "script", item })), + ...flows.map((item) => ({ kind: "flow", item })), + ...apps.map((item) => ({ kind: "app", item })), + ]; - // Process apps - for (const item of apps) { - current++; - try { - const result = await generateAppLocksInternal( - item.folder.replaceAll("/", SEP), - item.isRawApp!, // rawApp - false, // dryRun - workspace, - opts, - false, - true, // noStaleMessage - tree - ); - const appResult = result as AppLocksResult | undefined; - const scriptsInfo = appResult?.updatedScripts?.length - ? colors.dim(colors.white(`: ${appResult.updatedScripts.join(", ")}`)) - : ""; - log.info(`${formatProgress(current)} app ${item.path}${scriptsInfo}`); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - errors.push({ path: item.path, error: msg }); - log.info(`${formatProgress(current)} app ${item.path}`); - log.error(` Failed: ${msg}`); + // Buffer wmill-lock.yaml writes during the parallel phase: each task mutates + // the shared in-memory lockfile via clearGlobalLock/updateMetadataGlobalLock, + // then we flush once. Without this buffering, two workers' read-modify-write + // cycles would race and lose hashes. + await beginLockfileBatch(); + try { + const pool = new Set>(); + while (queue.length > 0 || pool.size > 0) { + while (pool.size < parallelism && queue.length > 0) { + const task = queue.shift()!; + const taskNumber = ++current; + const p = (async () => { + if (task.kind === "script") { + const item = task.item; + log.info(`${formatProgress(taskNumber)} script ${item.path}`); + try { + await generateScriptMetadataInternal( + item.path, // originalPath with extension + workspace, + opts, + false, // dryRun + true, // noStaleMessage + mismatchedWorkspaceDeps, + codebases, + false, + tree + ); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + errors.push({ path: item.path, error: msg }); + log.error(` Failed: ${msg}`); + } + } else if (task.kind === "flow") { + const item = task.item; + try { + const result = await generateFlowLockInternal( + item.folder.replaceAll("/", SEP), + false, // dryRun + workspace, + opts, + false, + true, // noStaleMessage + tree + ); + const flowResult = result as FlowLocksResult | undefined; + const scriptsInfo = flowResult?.updatedScripts?.length + ? colors.dim(colors.white(`: ${flowResult.updatedScripts.join(", ")}`)) + : ""; + log.info(`${formatProgress(taskNumber)} flow ${item.path}${scriptsInfo}`); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + errors.push({ path: item.path, error: msg }); + log.info(`${formatProgress(taskNumber)} flow ${item.path}`); + log.error(` Failed: ${msg}`); + } + } else { + const item = task.item; + try { + const result = await generateAppLocksInternal( + item.folder.replaceAll("/", SEP), + item.isRawApp!, // rawApp + false, // dryRun + workspace, + opts, + false, + true, // noStaleMessage + tree + ); + const appResult = result as AppLocksResult | undefined; + const scriptsInfo = appResult?.updatedScripts?.length + ? colors.dim(colors.white(`: ${appResult.updatedScripts.join(", ")}`)) + : ""; + log.info(`${formatProgress(taskNumber)} app ${item.path}${scriptsInfo}`); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + errors.push({ path: item.path, error: msg }); + log.info(`${formatProgress(taskNumber)} app ${item.path}`); + log.error(` Failed: ${msg}`); + } + } + })(); + pool.add(p); + p.then(() => pool.delete(p)); + } + if (pool.size > 0) { + await Promise.race(pool); + } } - } - // Persist all stale workspace dep hashes (not just filtered — deps are global, not folder-scoped) - const allStaleDeps = staleItems.filter((i) => i.type === "dependencies"); - await tree.persistDepsHashes(allStaleDeps.map((d) => d.path)); + // Persist all stale workspace dep hashes (not just filtered — deps are global, not folder-scoped) + const allStaleDeps = staleItems.filter((i) => i.type === "dependencies"); + await tree.persistDepsHashes(allStaleDeps.map((d) => d.path)); + } finally { + await flushLockfileBatch(); + } const succeeded = total - errors.length; log.info(""); @@ -640,6 +707,7 @@ const command = new Command() .option("--skip-flows", "Skip processing flows") .option("--skip-apps", "Skip processing apps") .option("--strict-folder-boundaries", "Only update items inside the specified folder (requires folder argument)") + .option("--parallel ", "Number of items to process in parallel") .option( "-i --includes ", "Comma separated patterns to specify which files to include" @@ -661,6 +729,7 @@ const command = new Command() .option("--skip-scripts", "Skip processing scripts") .option("--skip-flows", "Skip processing flows") .option("--skip-apps", "Skip processing apps") + .option("--parallel ", "Number of items to process in parallel") .option( "-i --includes ", "Comma separated patterns to specify which files to include" diff --git a/cli/src/commands/script/script.ts b/cli/src/commands/script/script.ts index 0635cd42ea..f1e605d8ff 100644 --- a/cli/src/commands/script/script.ts +++ b/cli/src/commands/script/script.ts @@ -12,7 +12,8 @@ import * as log from "../../core/log.ts"; import { sep as SEP } from "node:path"; import * as path from "node:path"; import { stringify as yamlStringify } from "yaml"; -import { deepEqual, readTextFile, readTextFileSync } from "../../utils/utils.ts"; +import { deepEqual, getHeaders, readTextFile, readTextFileSync } from "../../utils/utils.ts"; +import { detectAuthGatewayChallenge } from "../../utils/http_guards.ts"; import * as wmill from "../../../gen/services.gen.ts"; import * as specificItems from "../../core/specific_items.ts"; import { getCurrentGitBranch } from "../../utils/git.ts"; @@ -725,6 +726,7 @@ async function createScript( // (same content, lockfile, and metadata) as a no-op, so the CLI does not // produce phantom git-sync / promotion commits on re-pushes. const skipIfNoop = "skip_if_noop=true"; + const extraHeaders = getHeaders(); if (!bundleContent) { try { const url = @@ -738,9 +740,11 @@ async function createScript( headers: { Authorization: `Bearer ${workspace.token}`, "Content-Type": "application/json", + ...extraHeaders, }, body: JSON.stringify(body), }); + await detectAuthGatewayChallenge(req, url); if (req.status != 201) { throw Error( `${req.status} - ${req.statusText} - ${await req.text()}` @@ -771,9 +775,13 @@ async function createScript( skipIfNoop; const req = await fetch(url, { method: "POST", - headers: { Authorization: `Bearer ${workspace.token} ` }, + headers: { + Authorization: `Bearer ${workspace.token} `, + ...extraHeaders, + }, body: form, }); + await detectAuthGatewayChallenge(req, url); if (req.status != 201) { throw Error( `Script snapshot creation was not successful: ${req.status} - ${ @@ -1587,12 +1595,18 @@ async function preview( workspace.workspaceId + "/jobs/run/preview_bundle"; + const extraHeaders = getHeaders(); const response = await fetch(url, { method: "POST", - headers: { Authorization: `Bearer ${workspace.token}` }, + headers: { + Authorization: `Bearer ${workspace.token}`, + ...extraHeaders, + }, body: form, }); + await detectAuthGatewayChallenge(response, url); + if (!response.ok) { throw new Error( `Preview failed: ${response.status} - ${response.statusText} - ${await response.text()}` diff --git a/cli/src/guidance/skills.gen.ts b/cli/src/guidance/skills.gen.ts index 3eb1c872cc..2b0962e09d 100644 --- a/cli/src/guidance/skills.gen.ts +++ b/cli/src/guidance/skills.gen.ts @@ -5420,15 +5420,15 @@ Reference a specific resource using \`$res:\` prefix: ## OpenFlow Schema -{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-' (e.g., 'resource-stripe')"},"on_behalf_of_email":{"type":"string","description":"The flow will be run with the permissions of the user with this email."}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"debounce_args_to_accumulate":{"type":"array","description":"Arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds that a job can be debounced"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of times a job can be debounced"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"delete_after_secs":{"type":"integer","description":"If set, delete the flow job's args, result and logs after this many seconds following job completion"},"flow_env":{"type":"object","description":"Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource).","additionalProperties":{}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}},"groups":{"type":"array","description":"Semantic groups of modules for organizational purposes","items":{"$ref":"#/components/schemas/FlowGroup"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \\"yellow\\", \\"#ffff00\\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"FlowGroup":{"type":"object","description":"A semantic group of flow modules for organizational purposes. Does not affect execution \\u2014 modules remain in their original position in the flow. Groups provide naming and collapsibility in the editor. Members are computed dynamically from all nodes on paths between start_id and end_id.","properties":{"summary":{"type":"string","description":"Display name for this group"},"note":{"type":"string","description":"Markdown note shown below the group header"},"autocollapse":{"type":"boolean","default":false,"description":"If true, this group is collapsed by default in the flow editor. UI hint only."},"start_id":{"type":"string","description":"ID of the first flow module in this group (topological entry point)"},"end_id":{"type":"string","description":"ID of the last flow module in this group (topological exit point)"},"color":{"type":"string","description":"Color for the group in the flow editor"}},"required":["start_id","end_id"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","nullable":true,"description":"Custom error message when stopping with an error. Mutually exclusive with skip_if_stopped. If set to a non-empty string, the flow stops with this error. If empty string, a default error message is used. If null or omitted, no error is raised."}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_secs":{"type":"integer","description":"If set, delete the step's args, result and logs after this many seconds following job completion"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"},"debouncing":{"description":"Debounce configuration for this step (EE only)","type":"object","properties":{"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce this step's executions across flow runs"},"debounce_key":{"type":"string","description":"Expression to group debounced executions. Supports $workspace and $args[name]. Default: $workspace/flow/-"},"debounce_args_to_accumulate":{"type":"array","description":"Array-type arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds before forced execution"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of debounces before forced execution"}}}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"AiTransform":{"type":"object","description":"Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.","properties":{"type":{"type":"string","enum":["ai"]}},"required":["type"]},"AIProviderKind":{"type":"string","description":"Supported AI provider types","enum":["openai","azure_openai","anthropic","mistral","deepseek","googleai","groq","openrouter","togetherai","customai","aws_bedrock"]},"ProviderConfig":{"type":"object","description":"Complete AI provider configuration with resource reference and model selection","properties":{"kind":{"$ref":"#/components/schemas/AIProviderKind"},"resource":{"type":"string","description":"Resource reference in format '$res:{resource_path}' pointing to provider credentials"},"model":{"type":"string","description":"Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229', 'gemini-pro')"}},"required":["kind","resource","model"]},"StaticProviderTransform":{"type":"object","description":"Static provider configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/ProviderConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"ProviderTransform":{"description":"Provider configuration - can be static (ProviderConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticProviderTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticProviderTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"MemoryOff":{"type":"object","description":"No conversation memory/context","properties":{"kind":{"type":"string","enum":["off"]}},"required":["kind"]},"MemoryAuto":{"type":"object","description":"Automatic context management","properties":{"kind":{"type":"string","enum":["auto"]},"context_length":{"type":"integer","description":"Maximum number of messages to retain in context"},"memory_id":{"type":"string","description":"Identifier for persistent memory across agent invocations"}},"required":["kind"]},"MemoryMessage":{"type":"object","description":"A single message in conversation history","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}},"required":["role","content"]},"MemoryManual":{"type":"object","description":"Explicit message history","properties":{"kind":{"type":"string","enum":["manual"]},"messages":{"type":"array","items":{"$ref":"#/components/schemas/MemoryMessage"}}},"required":["kind","messages"]},"MemoryConfig":{"description":"Conversation memory configuration","oneOf":[{"$ref":"#/components/schemas/MemoryOff"},{"$ref":"#/components/schemas/MemoryAuto"},{"$ref":"#/components/schemas/MemoryManual"}],"discriminator":{"propertyName":"kind","mapping":{"off":"#/components/schemas/MemoryOff","auto":"#/components/schemas/MemoryAuto","manual":"#/components/schemas/MemoryManual"}}},"StaticMemoryTransform":{"type":"object","description":"Static memory configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/MemoryConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"MemoryTransform":{"description":"Memory configuration - can be static (MemoryConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticMemoryTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticMemoryTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type","mapping":{"rawscript":"#/components/schemas/RawScript","script":"#/components/schemas/PathScript","flow":"#/components/schemas/PathFlow","forloopflow":"#/components/schemas/ForloopFlow","whileloopflow":"#/components/schemas/WhileloopFlow","branchone":"#/components/schemas/BranchOne","branchall":"#/components/schemas/BranchAll","identity":"#/components/schemas/Identity","aiagent":"#/components/schemas/AiAgent"}}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php","rust","ansible","csharp","nu","java","ruby","rlang","duckdb"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable","volume"]},"access_type":{"type":"string","nullable":true,"description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","nullable":true,"description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"},{"$ref":"#/components/schemas/WebsearchToolValue"}],"discriminator":{"propertyName":"tool_type","mapping":{"flowmodule":"#/components/schemas/FlowModuleTool","mcp":"#/components/schemas/McpToolValue","websearch":"#/components/schemas/WebsearchToolValue"}}},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"WebsearchToolValue":{"type":"object","description":"A tool implemented as a websearch tool. The AI can call this like any other websearch tool","properties":{"tool_type":{"type":"string","enum":["websearch"]}},"required":["tool_type"]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/ProviderTransform"},"output_type":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Output format type.\\nValid values: 'text' (default) - plain text response, 'image' - image generation\\n"},"user_message":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"The user's prompt/message to the AI agent. Supports variable interpolation with flow.input syntax."},"system_prompt":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"System instructions that guide the AI's behavior, persona, and response style. Optional."},"streaming":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Boolean. If true, stream the AI response incrementally.\\nStreaming events include: token_delta, tool_call, tool_call_arguments, tool_execution, tool_result\\n"},"memory":{"$ref":"#/components/schemas/MemoryTransform"},"output_schema":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"JSON Schema object defining structured output format. Used when you need the AI to return data in a specific shape.\\nSupports standard JSON Schema properties: type, properties, required, items, enum, pattern, minLength, maxLength, minimum, maximum, etc.\\nExample: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }\\n"},"user_attachments":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Array of file references (images or PDFs) for the AI agent.\\nFormat: Array<{ bucket: string, key: string }> - S3 object references\\nExample: [{ bucket: 'my-bucket', key: 'documents/report.pdf' }]\\n"},"max_completion_tokens":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Integer. Maximum number of tokens the AI will generate in its response.\\nRange: 1 to 4,294,967,295. Typical values: 256-4096 for most use cases.\\n"},"temperature":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Float. Controls randomness/creativity of responses.\\nRange: 0.0 to 2.0 (provider-dependent)\\n- 0.0 = deterministic, focused responses\\n- 0.7 = balanced (common default)\\n- 1.0+ = more creative/random\\n"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"omit_output_from_conversation":{"type":"boolean","default":false,"description":"If true, this AI agent step does not persist its assistant or tool messages to the flow conversation when chat mode is enabled."},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"itered_len":{"type":"integer"},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["web_search"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}}`, +{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-' (e.g., 'resource-stripe')"},"on_behalf_of_email":{"type":"string","description":"The flow will be run with the permissions of the user with this email."}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"debounce_args_to_accumulate":{"type":"array","description":"Arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds that a job can be debounced"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of times a job can be debounced"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"delete_after_secs":{"type":"integer","description":"If set, delete the flow job's args, result and logs after this many seconds following job completion"},"flow_env":{"type":"object","description":"Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource).","additionalProperties":{}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}},"groups":{"type":"array","description":"Semantic groups of modules for organizational purposes","items":{"$ref":"#/components/schemas/FlowGroup"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \\"yellow\\", \\"#ffff00\\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"FlowGroup":{"type":"object","description":"A semantic group of flow modules for organizational purposes. Does not affect execution \\u2014 modules remain in their original position in the flow. Groups provide naming and collapsibility in the editor. Members are computed dynamically from all nodes on paths between start_id and end_id.","properties":{"summary":{"type":"string","description":"Display name for this group"},"note":{"type":"string","description":"Markdown note shown below the group header"},"autocollapse":{"type":"boolean","default":false,"description":"If true, this group is collapsed by default in the flow editor. UI hint only."},"start_id":{"type":"string","description":"ID of the first flow module in this group (topological entry point)"},"end_id":{"type":"string","description":"ID of the last flow module in this group (topological exit point)"},"color":{"type":"string","description":"Color for the group in the flow editor"}},"required":["start_id","end_id"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","nullable":true,"description":"Custom error message when stopping with an error. Mutually exclusive with skip_if_stopped. If set to a non-empty string, the flow stops with this error. If empty string, a default error message is used. If null or omitted, no error is raised."}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_secs":{"type":"integer","description":"If set, delete the step's args, result and logs after this many seconds following job completion"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"},"debouncing":{"description":"Debounce configuration for this step (EE only)","type":"object","properties":{"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce this step's executions across flow runs"},"debounce_key":{"type":"string","description":"Expression to group debounced executions. Supports $workspace and $args[name]. Default: $workspace/flow/-"},"debounce_args_to_accumulate":{"type":"array","description":"Array-type arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds before forced execution"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of debounces before forced execution"}}}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"AiTransform":{"type":"object","description":"Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.","properties":{"type":{"type":"string","enum":["ai"]}},"required":["type"]},"AIProviderKind":{"type":"string","description":"Supported AI provider types","enum":["openai","azure_openai","anthropic","mistral","deepseek","googleai","groq","openrouter","togetherai","customai","aws_bedrock"]},"ProviderConfig":{"type":"object","description":"Complete AI provider configuration with resource reference and model selection","properties":{"kind":{"$ref":"#/components/schemas/AIProviderKind"},"resource":{"type":"string","description":"Resource reference in format '$res:{resource_path}' pointing to provider credentials"},"model":{"type":"string","description":"Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229', 'gemini-pro')"}},"required":["kind","resource","model"]},"StaticProviderTransform":{"type":"object","description":"Static provider configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/ProviderConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"ProviderTransform":{"description":"Provider configuration - can be static (ProviderConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticProviderTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticProviderTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"MemoryOff":{"type":"object","description":"No conversation memory/context","properties":{"kind":{"type":"string","enum":["off"]}},"required":["kind"]},"MemoryAuto":{"type":"object","description":"Automatic context management","properties":{"kind":{"type":"string","enum":["auto"]},"context_length":{"type":"integer","description":"Maximum number of messages to retain in context"},"memory_id":{"type":"string","description":"Identifier for persistent memory across agent invocations"}},"required":["kind"]},"MemoryMessage":{"type":"object","description":"A single message in conversation history","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}},"required":["role","content"]},"MemoryManual":{"type":"object","description":"Explicit message history","properties":{"kind":{"type":"string","enum":["manual"]},"messages":{"type":"array","items":{"$ref":"#/components/schemas/MemoryMessage"}}},"required":["kind","messages"]},"MemoryConfig":{"description":"Conversation memory configuration","oneOf":[{"$ref":"#/components/schemas/MemoryOff"},{"$ref":"#/components/schemas/MemoryAuto"},{"$ref":"#/components/schemas/MemoryManual"}],"discriminator":{"propertyName":"kind","mapping":{"off":"#/components/schemas/MemoryOff","auto":"#/components/schemas/MemoryAuto","manual":"#/components/schemas/MemoryManual"}}},"StaticMemoryTransform":{"type":"object","description":"Static memory configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/MemoryConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"MemoryTransform":{"description":"Memory configuration - can be static (MemoryConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticMemoryTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticMemoryTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type","mapping":{"rawscript":"#/components/schemas/RawScript","script":"#/components/schemas/PathScript","flow":"#/components/schemas/PathFlow","forloopflow":"#/components/schemas/ForloopFlow","whileloopflow":"#/components/schemas/WhileloopFlow","branchone":"#/components/schemas/BranchOne","branchall":"#/components/schemas/BranchAll","identity":"#/components/schemas/Identity","aiagent":"#/components/schemas/AiAgent"}}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php","rust","ansible","csharp","nu","java","ruby","rlang","duckdb"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable","volume"]},"access_type":{"type":"string","nullable":true,"description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","nullable":true,"description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"},{"$ref":"#/components/schemas/WebsearchToolValue"}],"discriminator":{"propertyName":"tool_type","mapping":{"flowmodule":"#/components/schemas/FlowModuleTool","mcp":"#/components/schemas/McpToolValue","websearch":"#/components/schemas/WebsearchToolValue"}}},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"WebsearchToolValue":{"type":"object","description":"A tool implemented as a websearch tool. The AI can call this like any other websearch tool","properties":{"tool_type":{"type":"string","enum":["websearch"]}},"required":["tool_type"]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/ProviderTransform"},"output_type":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Output format type.\\nValid values: 'text' (default) - plain text response, 'image' - image generation\\n"},"user_message":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"The user's prompt/message to the AI agent. Supports variable interpolation with flow.input syntax."},"system_prompt":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"System instructions that guide the AI's behavior, persona, and response style. Optional."},"streaming":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Boolean. If true, stream the AI response incrementally.\\nStreaming events include: token_delta, tool_call, tool_call_arguments, tool_execution, tool_result\\n"},"memory":{"$ref":"#/components/schemas/MemoryTransform"},"output_schema":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"JSON Schema object defining structured output format. Used when you need the AI to return data in a specific shape.\\nSupports standard JSON Schema properties: type, properties, required, items, enum, pattern, minLength, maxLength, minimum, maximum, etc.\\nExample: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }\\n"},"user_attachments":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Array of file references (images or PDFs) for the AI agent.\\nFormat: Array<{ bucket: string, key: string }> - S3 object references\\nExample: [{ bucket: 'my-bucket', key: 'documents/report.pdf' }]\\n"},"max_completion_tokens":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Integer. Maximum number of tokens the AI will generate in its response.\\nRange: 1 to 4,294,967,295. Typical values: 256-4096 for most use cases.\\n"},"temperature":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Float. Controls randomness/creativity of responses.\\nRange: 0.0 to 2.0 (provider-dependent)\\n- 0.0 = deterministic, focused responses\\n- 0.7 = balanced (common default)\\n- 1.0+ = more creative/random\\n"},"max_iterations":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Number. Limits how many times the agent can loop through reasoning and tool use.\\nRange: 1-1000.\\n"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"omit_output_from_conversation":{"type":"boolean","default":false,"description":"If true, this AI agent step does not persist its assistant or tool messages to the flow conversation when chat mode is enabled."},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"itered_len":{"type":"integer"},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["web_search"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}}`, "raw-app": `--- name: raw-app description: MUST use when creating raw apps. --- -# Windmill Raw Apps +# Windmill Raw Apps — CLI workflow -Raw apps let you build custom frontends with React, Svelte, or Vue that connect to Windmill backend runnables and datatables. +This guide covers raw apps from the terminal: scaffolding via \`wmill app new\`, the on-disk layout, and the file-based conventions the CLI uses to represent backend runnables and data table configuration. The platform shape (how a raw app behaves at runtime — frontend bundling, runnable types, datatable SDK calls) is covered in the companion authoring guide. ## Creating a Raw App @@ -5497,7 +5497,7 @@ wmill app new This is the wizard. It only works when run by a human in a real terminal. Don't call it this way from an agent. -## App Structure +## On-disk app layout \`\`\` my_app{{RAW_APP_SUFFIX}}/ @@ -5517,11 +5517,7 @@ my_app{{RAW_APP_SUFFIX}}/ └── *.sql # SQL files to apply via dev server \`\`\` -## Backend Runnables - -Backend runnables are server-side scripts that your frontend can call. They live in the \`backend/\` folder. - -### Creating a Backend Runnable +## Backend runnables on disk Add a code file to the \`backend/\` folder: @@ -5531,7 +5527,7 @@ backend/. The runnable ID is the filename without extension. For example, \`get_user.ts\` creates a runnable with ID \`get_user\`. -### Supported Languages +### Supported languages (extension-driven) | Language | Extension | Example | |------------------|--------------|------------------| @@ -5553,27 +5549,14 @@ The runnable ID is the filename without extension. For example, \`get_user.ts\` | C# | \`.cs\` | \`myFunc.cs\` | | Java | \`.java\` | \`myFunc.java\` | -### Example Backend Runnable - -**backend/get_user.ts:** -\`\`\`typescript -import * as wmill from 'windmill-client'; - -export async function main(user_id: string) { - const sql = wmill.datatable(); - const user = await sql\`SELECT * FROM users WHERE id = \${user_id}\`.fetchOne(); - return user; -} -\`\`\` - -After creating, tell the user they can generate lock files by running: +After creating a runnable, tell the user they can generate lock files by running: \`\`\`bash wmill generate-metadata \`\`\` -### Optional YAML Configuration +### Optional YAML configuration -Add a \`.yaml\` file to configure fields or static values: +Add a \`.yaml\` file alongside the code to configure fields or static values: **backend/get_user.yaml:** \`\`\`yaml @@ -5584,7 +5567,7 @@ fields: value: "default_user" \`\`\` -### Referencing Existing Scripts +### Referencing existing scripts To use an existing Windmill script instead of inline code: @@ -5600,32 +5583,9 @@ type: flow path: f/my_folder/my_flow \`\`\` -### Calling Backend from Frontend +## Data tables — \`raw_app.yaml\` config -Import from the auto-generated \`wmill.ts\`: - -\`\`\`typescript -import { backend } from './wmill'; - -// Call a backend runnable -const user = await backend.get_user({ user_id: '123' }); -\`\`\` - -The \`wmill.ts\` file provides type-safe access to all backend runnables. - -## Data Tables - -Raw apps can query Windmill datatables (PostgreSQL databases managed by Windmill). - -### Critical Rules - -1. **ONLY USE WHITELISTED TABLES**: You can ONLY query tables listed in \`raw_app.yaml\` → \`data.tables\`. Tables not in this list are NOT accessible. - -2. **ADD TABLES BEFORE USING**: To use a new table, first add it to \`data.tables\` in \`raw_app.yaml\`. - -3. **USE CONFIGURED DATATABLE/SCHEMA**: Check the app's \`raw_app.yaml\` for the default datatable and schema. - -### Configuration in raw_app.yaml +The \`data\` block in \`raw_app.yaml\` controls which tables the app can query. \`\`\`yaml data: @@ -5637,9 +5597,153 @@ data: \`\`\` **Table reference formats:** -- \`\` - All tables in the datatable -- \`/\` - Specific table in public schema -- \`/:
\` - Table in specific schema +- \`\` — All tables in the datatable +- \`/
\` — Specific table in public schema +- \`/:
\` — Table in specific schema + +## SQL Migrations (sql_to_apply/) + +The \`sql_to_apply/\` folder is for creating/modifying database tables during development. + +### Workflow + +1. Create \`.sql\` files in \`sql_to_apply/\` +2. Run \`wmill app dev\` — the dev server watches this folder +3. When SQL files change, a modal appears in the browser to confirm execution +4. After creating tables, **add them to \`data.tables\`** in \`raw_app.yaml\` + +### Example migration + +**sql_to_apply/001_create_users.sql:** +\`\`\`sql +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + name TEXT, + created_at TIMESTAMP DEFAULT NOW() +); +\`\`\` + +After applying, add to \`raw_app.yaml\`: +\`\`\`yaml +data: + tables: + - main/users +\`\`\` + +### Migration best practices + +- **Use idempotent SQL**: \`CREATE TABLE IF NOT EXISTS\`, etc. +- **Number files**: \`001_\`, \`002_\` for ordering +- **Always whitelist tables** after creation +- This folder is NOT synced — it's for local development only + +## CLI Commands + +\`wmill app new\` is the exception: you run it yourself, with flags, per the "Creating a Raw App" section above. + +For everything else, tell the user which command fits their intent and let them run it — these touch the workspace or local lock files, and the user should consent each time: + +| Command | Description | +|---------|-------------| +| \`wmill app dev\` | Start dev server with live reload (see the \`preview\` skill for the full open-the-app-in-the-IDE-pane procedure). | +| \`wmill app generate-agents\` | Refresh AGENTS.md and DATATABLES.md | +| \`wmill generate-metadata\` | Generate lock files for backend runnables | +| \`wmill sync push\` | Deploy app to Windmill | +| \`wmill sync pull\` | Pull latest from Windmill | + + + +# Windmill Raw Apps + +Raw apps let you build custom frontends with React, Svelte, or Vue that connect to Windmill backend runnables and datatables. + +## App shape + +A raw app has three logical parts: + +- **Frontend** — bundled with esbuild from \`index.tsx\` as the entrypoint. Files include the entrypoint, components (\`App.tsx\`), styles, etc. +- **Backend runnables** — server-side scripts the frontend calls, each addressed by a unique key. +- **Data** — optional whitelisted datatables (managed PostgreSQL) that the backend runnables can query. The frontend never queries the database directly; backend runnables are the only bridge. + +## Frontend + +### Entrypoint + +\`index.tsx\` is the bundling entrypoint. It typically renders a top-level \`App\` component. The bundler is esbuild. + +### Generated bindings (\`wmill.d.ts\` / \`wmill.ts\`) + +The frontend imports a generated module that mirrors the backend runnables. **Never write to it directly** — it gets regenerated whenever backend runnables change. Modifying it by hand will be overwritten. + +### Calling backend runnables + +Import the generated bindings and call the runnable like a function: + +\`\`\`typescript +import { backend } from './wmill'; + +// Call a backend runnable +const user = await backend.get_user({ user_id: '123' }); +\`\`\` + +The frontend cannot reach datatables, workspace items, or external services on its own — it goes through \`backend.(args)\` for everything server-side. + +## Backend runnables + +Each runnable has a unique key (used to call it from the frontend) and one of four types: + +| Type | What it is | +|---|---| +| \`inline\` | Custom code stored on the app itself. Most common for app-specific logic. | +| \`script\` | Reference to an existing workspace script by path. | +| \`flow\` | Reference to an existing workspace flow by path. | +| \`hubscript\` | Reference to a hub script by path. | + +### Inline runnables + +Inline runnables carry their own source code. For file-based raw apps, the runnable language is determined by the backend file extension. The script must expose a \`main\` function as its entrypoint. + +**TypeScript example** (\`backend/get_user.ts\`): + +\`\`\`typescript +import * as wmill from 'windmill-client'; + +export async function main(user_id: string) { + const sql = wmill.datatable(); + const user = await sql\`SELECT * FROM users WHERE id = \${user_id}\`.fetchOne(); + return user; +} +\`\`\` + +**Python example** (\`backend/get_user.py\`): + +\`\`\`python +import wmill + +def main(user_id: str): + db = wmill.datatable() + user = db.query('SELECT * FROM users WHERE id = $1', user_id).fetch_one() + return user +\`\`\` + +### Path runnables (script / flow / hubscript) + +When \`type\` is \`script\`, \`flow\`, or \`hubscript\`, the runnable just stores a \`path\` to an existing workspace or hub item — no inline code. The referenced item's input/output schema becomes the runnable's surface. + +### Static inputs + +\`staticInputs\` is an optional \`Record\` for arguments not overridable from the frontend. Useful with path runnables to pre-fill some args while leaving the rest to the frontend caller. + +## Data Tables + +Data tables are PostgreSQL databases managed by Windmill. Backend runnables query them via the \`wmill\` client; the frontend never queries them directly. + +### Critical rules + +1. **Whitelisted tables only**: a runnable can only query tables listed in the app's \`data.tables\` config. Tables not in this list are not accessible. +2. **Add tables before using**: queries against unlisted tables fail at runtime. When you introduce a new table, register it in \`data.tables\` first. +3. **Use the configured datatable/schema**: the app's \`data\` config sets the default datatable and schema; reference them consistently across runnables. ### Querying in TypeScript (Bun/Deno) @@ -5680,65 +5784,13 @@ def main(user_id: str): return user \`\`\` -## SQL Migrations (sql_to_apply/) - -The \`sql_to_apply/\` folder is for creating/modifying database tables during development. - -### Workflow - -1. Create \`.sql\` files in \`sql_to_apply/\` -2. Run \`wmill app dev\` - the dev server watches this folder -3. When SQL files change, a modal appears in the browser to confirm execution -4. After creating tables, **add them to \`data.tables\`** in \`raw_app.yaml\` - -### Example Migration - -**sql_to_apply/001_create_users.sql:** -\`\`\`sql -CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, - email TEXT NOT NULL UNIQUE, - name TEXT, - created_at TIMESTAMP DEFAULT NOW() -); -\`\`\` - -After applying, add to \`raw_app.yaml\`: -\`\`\`yaml -data: - tables: - - main/users -\`\`\` - -### Migration Best Practices - -- **Use idempotent SQL**: \`CREATE TABLE IF NOT EXISTS\`, etc. -- **Number files**: \`001_\`, \`002_\` for ordering -- **Always whitelist tables** after creation -- This folder is NOT synced - it's for local development only - -## CLI Commands - -\`wmill app new\` is the exception: you run it yourself, with flags, per the "Creating a Raw App" section above. - -For everything else, tell the user which command fits their intent and let them run it — these touch the workspace or local lock files, and the user should consent each time: - -| Command | Description | -|---------|-------------| -| \`wmill app dev\` | Start dev server with live reload (see the \`preview\` skill for the full open-the-app-in-the-IDE-pane procedure). | -| \`wmill app generate-agents\` | Refresh AGENTS.md and DATATABLES.md | -| \`wmill generate-metadata\` | Generate lock files for backend runnables | -| \`wmill sync push\` | Deploy app to Windmill | -| \`wmill sync pull\` | Pull latest from Windmill | - ## Best Practices -1. **Check DATATABLES.md** for existing tables before creating new ones -2. **Use parameterized queries** - never concatenate user input into SQL -3. **Keep runnables focused** - one function per file -4. **Use descriptive IDs** - \`get_user.ts\` not \`a.ts\` -5. **Always whitelist tables** - add to \`data.tables\` before querying -6. **Generate locks** - tell the user to run \`wmill generate-metadata\` after adding/modifying backend runnables +1. **Check existing tables** before creating new ones — reuse beats schema growth. +2. **Use parameterized queries** — never concatenate user input into SQL. +3. **Keep runnables focused** — one function per runnable; small surface area. +4. **Use descriptive keys** — \`get_user\`, not \`a\`. +5. **Always whitelist tables** — adding a runnable that queries a new table requires the table to be in \`data.tables\` first. `, "triggers": `--- name: triggers @@ -6588,7 +6640,7 @@ app related commands - \`--json\` - Output as JSON (for piping to jq) - \`app get \` - get an app's details - \`--json\` - Output as JSON (for piping to jq) -- \`app push \` - push a local app +- \`app push [file_path:string] [remote_path:string]\` - push a local app. With no args, infers the app from the current directory and the remote path from its location relative to wmill.yaml. - \`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 @@ -6730,6 +6782,7 @@ Generate metadata (locks, schemas) for all scripts, flows, and apps - \`--skip-flows\` - Skip processing flows - \`--skip-apps\` - Skip processing apps - \`--strict-folder-boundaries\` - Only update items inside the specified folder (requires folder argument) +- \`--parallel \` - Number of items to process in parallel - \`-i --includes \` - Comma separated patterns to specify which files to include - \`-e --excludes \` - Comma separated patterns to specify which files to exclude @@ -6739,6 +6792,7 @@ Generate metadata (locks, schemas) for all scripts, flows, and apps - \`--skip-scripts\` - Skip processing scripts - \`--skip-flows\` - Skip processing flows - \`--skip-apps\` - Skip processing apps + - \`--parallel \` - Number of items to process in parallel - \`-i --includes \` - Comma separated patterns to specify which files to include - \`-e --excludes \` - Comma separated patterns to specify which files to exclude diff --git a/cli/src/main.ts b/cli/src/main.ts index 3d9a15dfd9..a755c16901 100755 --- a/cli/src/main.ts +++ b/cli/src/main.ts @@ -79,7 +79,7 @@ export { token, }; -export const VERSION = "1.697.0"; +export const VERSION = "1.700.2"; // Re-exported from constants.ts to maintain backwards compatibility export { WM_FORK_PREFIX } from "./core/constants.ts"; diff --git a/cli/src/utils/metadata.ts b/cli/src/utils/metadata.ts index 456336dae2..9668dd19e6 100644 --- a/cli/src/utils/metadata.ts +++ b/cli/src/utils/metadata.ts @@ -1202,7 +1202,32 @@ export function normalizeLockPath(p: string): string { return n; } +// When set, `clearGlobalLock` and `updateMetadataGlobalLock` mutate this +// in-memory copy instead of doing a full read-modify-write on disk for every +// call. Callers that fan out item processing through a worker pool wrap the +// pool with `beginLockfileBatch()`/`flushLockfileBatch()` so the lockfile is +// only written once at the end — see `generate-metadata` parallelism. +let inMemoryLock: Lock | null = null; + +export async function beginLockfileBatch(): Promise { + if (inMemoryLock) return; + inMemoryLock = await readLockfile(); +} + +export async function flushLockfileBatch(): Promise { + if (!inMemoryLock) return; + // Write first, then clear: if the disk write throws (e.g. ENOSPC), the + // buffered updates remain in memory and a retry can re-attempt the flush. + await writeFile( + WMILL_LOCKFILE, + yamlStringify(inMemoryLock as Record, yamlOptions), + "utf-8", + ); + inMemoryLock = null; +} + export async function readLockfile(): Promise { + if (inMemoryLock) return inMemoryLock; let parsed: unknown; try { parsed = await yamlParseFile(WMILL_LOCKFILE); @@ -1344,11 +1369,13 @@ export async function clearGlobalLock(path: string): Promise { } }); } - await writeFile( - WMILL_LOCKFILE, - yamlStringify(conf as Record, yamlOptions), - "utf-8" - ); + if (!inMemoryLock) { + await writeFile( + WMILL_LOCKFILE, + yamlStringify(conf as Record, yamlOptions), + "utf-8" + ); + } } } @@ -1377,9 +1404,11 @@ export async function updateMetadataGlobalLock( conf.locks[path] = hash; } } - await writeFile( - WMILL_LOCKFILE, - yamlStringify(conf as Record, yamlOptions), - "utf-8" - ); + if (!inMemoryLock) { + await writeFile( + WMILL_LOCKFILE, + yamlStringify(conf as Record, yamlOptions), + "utf-8" + ); + } } diff --git a/cli/test/headers_env_var.test.ts b/cli/test/headers_env_var.test.ts new file mode 100644 index 0000000000..72fdf4103f --- /dev/null +++ b/cli/test/headers_env_var.test.ts @@ -0,0 +1,264 @@ +/** + * Integration test: HEADERS env var is forwarded on every CLI fetch call. + * + * Spins up an auth-gateway proxy in front of the test backend that: + * - 403s + Cloudflare-style HTML if the request is missing CF-Access-Client-Id / + * CF-Access-Client-Secret (mirrors the real-world Cloudflare Access challenge), + * - otherwise reverse-proxies to the backend. + * + * Then runs `wmill sync push` with --base-url pointed at the proxy. If any fetch + * in the CLI bypasses HEADERS, the proxy returns the challenge page and the push + * fails (or the request is logged as un-authenticated). Negative case verifies + * the gateway actually rejects un-headered requests, so a passing positive case + * is meaningful. + * + * Regression coverage for #6421 and the script.ts pushScript / + * jobs/run/preview_bundle / app dev SSE fetch calls that used to skip getHeaders(). + */ + +import { expect, test } from "bun:test"; +import { mkdir, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import type { Server } from "bun"; +import { withTestBackend } from "./test_backend.ts"; + +const HEADER_NAMES = ["CF-Access-Client-Id", "CF-Access-Client-Secret"] as const; +const HEADER_VALUES = { + "CF-Access-Client-Id": "test-cf-id", + "CF-Access-Client-Secret": "test-cf-secret", +} as const; + +const HEADERS_ENV = HEADER_NAMES + .map((h) => `${h}: ${HEADER_VALUES[h]}`) + .join(", "); + +interface ProxyState { + authenticatedRequests: { method: string; path: string }[]; + rejectedRequests: { method: string; path: string }[]; +} + +interface RunningProxy { + server: Server; + state: ProxyState; + url: string; +} + +function startGatewayProxy(backendUrl: string): RunningProxy { + const state: ProxyState = { + authenticatedRequests: [], + rejectedRequests: [], + }; + + const server = Bun.serve({ + port: 0, + hostname: "127.0.0.1", + async fetch(req) { + const reqUrl = new URL(req.url); + const pathAndQuery = reqUrl.pathname + reqUrl.search; + + const id = req.headers.get("cf-access-client-id"); + const secret = req.headers.get("cf-access-client-secret"); + const passes = + id === HEADER_VALUES["CF-Access-Client-Id"] && + secret === HEADER_VALUES["CF-Access-Client-Secret"]; + + if (!passes) { + state.rejectedRequests.push({ method: req.method, path: pathAndQuery }); + return new Response( + 'Sign in ・ Cloudflare Access' + + 'Authenticate to reach this site.', + { + status: 403, + headers: { + "content-type": "text/html; charset=utf-8", + "cf-ray": "0000000000000000-TEST", + "cf-mitigated": "challenge", + }, + }, + ); + } + + state.authenticatedRequests.push({ method: req.method, path: pathAndQuery }); + + const target = new URL(pathAndQuery, backendUrl); + const forwardHeaders = new Headers(req.headers); + forwardHeaders.delete("cf-access-client-id"); + forwardHeaders.delete("cf-access-client-secret"); + forwardHeaders.set("host", new URL(backendUrl).host); + + const body = + req.method === "GET" || req.method === "HEAD" + ? undefined + : await req.arrayBuffer(); + + return await fetch(target, { + method: req.method, + headers: forwardHeaders, + body, + redirect: "manual", + }); + }, + }); + + return { + server, + state, + url: `http://127.0.0.1:${server.port}`, + }; +} + +async function runCliThroughProxy( + backend: { workspace: string; testConfigDir: string; token?: string }, + proxyUrl: string, + cliArgs: string[], + cwd: string, + env: Record, +): Promise<{ stdout: string; stderr: string; code: number }> { + // Use fileURLToPath + node:path so the CLI entrypoint is a real OS path on + // Windows. `new URL(..).pathname` yields `/C:/...` which Bun.spawn fails to + // resolve, so the negative case "rejected" on launch instead of reaching + // the proxy and the assertion on rejectedRequests > 0 flaked. + const cliDir = join(dirname(fileURLToPath(import.meta.url)), ".."); + const useNode = process.env["TEST_CLI_RUNTIME"] === "node"; + const runtime = useNode ? "node" : "bun"; + const entrypoint = useNode + ? join(cliDir, "npm", "esm", "main.js") + : join(cliDir, "src", "main.ts"); + const runtimeArgs = useNode ? [entrypoint] : ["run", entrypoint]; + + const fullArgs = [ + "--base-url", proxyUrl, + "--workspace", backend.workspace, + "--token", backend.token ?? "", + "--config-dir", backend.testConfigDir, + ...cliArgs, + ]; + + const proc = Bun.spawn([runtime, ...runtimeArgs, ...fullArgs], { + cwd, + env: { ...(process.env as Record), ...env }, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]); + const code = await proc.exited; + return { stdout, stderr, code }; +} + +test( + "HEADERS env var is forwarded on every CLI fetch (sync push of new script)", + async () => { + await withTestBackend(async (backend, tempDir) => { + const proxy = startGatewayProxy(backend.baseUrl); + try { + await writeFile( + `${tempDir}/wmill.yaml`, + `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`, + "utf-8", + ); + + const uniqueId = Date.now(); + const scriptName = `headers_${uniqueId}`; + const scriptDir = `${tempDir}/f/test`; + await mkdir(scriptDir, { recursive: true }); + await writeFile( + `${scriptDir}/${scriptName}.ts`, + `export async function main() {\n return "headers test ${uniqueId}";\n}\n`, + "utf-8", + ); + await writeFile( + `${scriptDir}/${scriptName}.script.yaml`, + [ + `summary: "headers regression"`, + `description: "Push covers /scripts/create + /jobs/run/dependencies_async"`, + `lock: ""`, + `schema:`, + ` $schema: "https://json-schema.org/draft/2020-12/schema"`, + ` type: object`, + ` properties: {}`, + ` required: []`, + `is_template: false`, + `kind: script`, + `language: bun`, + ``, + ].join("\n"), + "utf-8", + ); + + const includesGlob = `f/test/${scriptName}**`; + + // Negative case: no HEADERS env -> the proxy should return the + // gateway challenge and the CLI should bail out non-zero. Proves the + // proxy is actually gating, so the positive case isn't a false pass. + const noHeaders = await runCliThroughProxy( + backend, + proxy.url, + ["sync", "push", "--yes", "--includes", includesGlob], + tempDir, + {}, + ); + expect(noHeaders.code).not.toEqual(0); + expect(proxy.state.rejectedRequests.length).toBeGreaterThan(0); + expect(proxy.state.authenticatedRequests.length).toEqual(0); + + // Reset proxy state between cases. + proxy.state.authenticatedRequests.length = 0; + proxy.state.rejectedRequests.length = 0; + + // Positive case: HEADERS env set -> the proxy must see those headers + // on every request the CLI makes for sync push to succeed. + const withHeaders = await runCliThroughProxy( + backend, + proxy.url, + ["sync", "push", "--yes", "--includes", includesGlob], + tempDir, + { HEADERS: HEADERS_ENV }, + ); + expect(withHeaders.code).toEqual(0); + expect(proxy.state.rejectedRequests).toEqual([]); + + const paths = proxy.state.authenticatedRequests.map((r) => r.path); + + // Print the full path list when an assertion below fails so the + // failure is debuggable without re-running with extra logging. + const debug = () => paths.join("\n "); + + // Tarball download (sync diff): src/commands/sync/pull.ts + expect( + paths.some((p) => + p.startsWith(`/api/w/${backend.workspace}/workspaces/tarball`), + ), + `expected tarball request, got:\n ${debug()}`, + ).toBe(true); + + // Script create: src/commands/script/script.ts pushScript(). + // This is the regression: PR #8936 switched from wmill.createScript() + // (SDK, inherits OpenAPI.HEADERS) to a raw fetch that didn't forward + // HEADERS. Without the fix, the proxy would 403 this request and the + // CLI exit would be non-zero — so this assertion is the load-bearing + // one for #8936 / #6421-style regressions. + expect( + paths.some((p) => + p.includes(`/api/w/${backend.workspace}/scripts/create`), + ), + `expected /scripts/create request, got:\n ${debug()}`, + ).toBe(true); + + // Lock generation: src/utils/metadata.ts. Was the original #6421 fix + // (PR #6422) — a soft check; the backend may skip queueing a lock job + // for trivial bun scripts under some feature configurations, so we + // only verify it *if* the CLI tried to generate one. + // (No assertion — covered by the tarball+create checks above plus + // the negative case proving the proxy actually gates requests.) + } finally { + proxy.server.stop(true); + } + }); + }, + 240_000, +); diff --git a/cli/test/mixed_case_paths.test.ts b/cli/test/mixed_case_paths.test.ts index d85f81cc83..872c6f94ff 100644 --- a/cli/test/mixed_case_paths.test.ts +++ b/cli/test/mixed_case_paths.test.ts @@ -130,6 +130,57 @@ async function createFlow( throw new Error(`Failed to create flow ${flowPath}: ${error}`); } await response.text(); + + // Flow creation queues an async FlowDependencies job that generates the + // inline-script lockfile and rewrites flow.value. If we don't wait, a + // subsequent pull/push races the worker and the dry-run idempotency + // assertion sees a phantom lock-add + flow.yaml-edit diff (CI-only flake). + await waitForFlowDependencyJob(backend, flowPath); +} + +async function waitForFlowDependencyJob( + backend: any, + flowPath: string, + timeoutMs: number = 30000, +): Promise { + // /flows/get does not return `dependency_job`. The deployment_status route + // joins flow_version against deployment_metadata, which is populated in the + // same tx as the FlowDependencies push, so by the time the create/update + // API call returns, job_id is already the latest dep-job UUID. + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + const statusResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/flows/deployment_status/p/${flowPath}`, + ); + if (statusResp.status === 404) { + await statusResp.text().catch(() => {}); + return; + } + if (!statusResp.ok) { + await statusResp.text().catch(() => {}); + throw new Error( + `Failed to fetch deployment status for ${flowPath}: ${statusResp.status}`, + ); + } + const status = await statusResp.json(); + const depJobId: string | undefined = status?.job_id; + if (!depJobId) { + await new Promise((r) => setTimeout(r, 100)); + continue; + } + const completed = await backend.apiRequest!( + `/api/w/${backend.workspace}/jobs_u/completed/get/${depJobId}`, + ); + if (completed.ok) { + await completed.text(); + return; + } + await completed.text().catch(() => {}); + await new Promise((r) => setTimeout(r, 100)); + } + throw new Error( + `Flow dependency job for ${flowPath} did not complete within ${timeoutMs}ms`, + ); } async function createApp( @@ -384,6 +435,10 @@ excludes: [] const updatedFlow = await getFlow(backend, flowPath); expect(updatedFlow.summary).toEqual("Modified Data Processor Flow from test"); + // The CLI push enqueues a fresh FlowDependencies job. Wait for it before + // the dry-run pull so the lock/flow.value writes have committed. + await waitForFlowDependencyJob(backend, flowPath); + // Verify no diff on subsequent pull (idempotency) await verifyNoDiffOnPull(backend, tempDir); }); diff --git a/cli/test/sync_pull_push.test.ts b/cli/test/sync_pull_push.test.ts index e64af246f2..e31c4e1084 100644 --- a/cli/test/sync_pull_push.test.ts +++ b/cli/test/sync_pull_push.test.ts @@ -493,6 +493,58 @@ async function cleanupTempDir(dir: string): Promise { } } +// Polls /flows/deployment_status/p/{path} + /jobs_u/completed/get until the +// most recent flow dependency job has completed. After a flow create/update +// the API queues a FlowDependencies job that asynchronously fills inline- +// script lockfiles and rewrites flow.value; tests that round-trip through +// sync push/pull must wait for it or they race the worker (CI-only flake). +// `deployment_status` is the right read here — `/flows/get` does not return +// `dependency_job`, but the `deployment_metadata.job_id` row is written in +// the same tx as the dep-job push, so the returned `job_id` is the latest +// dep-job UUID by the time the create/update API call has returned. Returns +// silently if the flow doesn't exist on the server (treats it as a no-op +// push, since the route returns 404 when the flow row is missing). +async function waitForFlowDependencyJob( + backend: any, + flowPath: string, + timeoutMs: number = 30000, +): Promise { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + const statusResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/flows/deployment_status/p/${flowPath}`, + ); + if (statusResp.status === 404) { + await statusResp.text().catch(() => {}); + return; + } + if (!statusResp.ok) { + await statusResp.text().catch(() => {}); + throw new Error( + `Failed to fetch deployment status for ${flowPath}: ${statusResp.status}`, + ); + } + const status = await statusResp.json(); + const depJobId: string | undefined = status?.job_id; + if (!depJobId) { + await new Promise((r) => setTimeout(r, 100)); + continue; + } + const completed = await backend.apiRequest!( + `/api/w/${backend.workspace}/jobs_u/completed/get/${depJobId}`, + ); + if (completed.ok) { + await completed.text(); + return; + } + await completed.text().catch(() => {}); + await new Promise((r) => setTimeout(r, 100)); + } + throw new Error( + `Flow dependency job for ${flowPath} did not complete within ${timeoutMs}ms`, + ); +} + // ============================================================================= // Tests // ============================================================================= @@ -1797,6 +1849,12 @@ excludes: [] expect(pushResult.code).toEqual(0); + // sync push of the flow enqueues an async FlowDependencies job that + // generates the inline-script lockfile and rewrites flow.value. Wait for + // it before pulling back, otherwise pull races the worker and the + // dry-run push idempotency check sees phantom diffs (CI-only flake). + await waitForFlowDependencyJob(backend, flowName); + // Pull back const pullResult = await backend.runCLICommand( ["sync", "pull", "--yes", "--includes", `f/test/mixed_*_${uniqueId}**`], diff --git a/docs/windmill-ai-refactor-plan.md b/docs/windmill-ai-refactor-plan.md index 81ab5d7b86..0e8449d897 100644 --- a/docs/windmill-ai-refactor-plan.md +++ b/docs/windmill-ai-refactor-plan.md @@ -23,6 +23,58 @@ windmill-worker → windmill-ai windmill-common does **NOT** re-export from windmill-ai (would be circular). All consumers update imports. +## Reviewer Note: Keep the Next PR Small + +The first merged PR established the crate boundary; it did not yet remove the duplicated API-vs-worker provider paths. The remaining work should stay split by dependency risk, not by the final desired module layout. + +Do not jump directly from the current state to provider moves, proxy unification, and credential unification in one PR. The riskiest part is the API proxy because it combines request transformation, endpoint selection, auth headers, custom headers, OAuth user injection, Azure URL handling, Anthropic Vertex handling, Bedrock SDK calls, and SSE keepalive behavior. + +Pull the shared plumbing forward before moving provider implementations: +- Move tiny shared utilities first, including `AI_HTTP_HEADERS`, `extract_text_content`, and `should_use_structured_output_tool`. +- Move SSE parsers next, using the existing `StreamEventSink` abstraction, and update callers to import from `windmill_ai` directly. +- Leave provider implementations, image upload/download handling, API proxy changes, and credential unification out of that PR. + +Avoid adding modules whose only purpose is to re-export moved code. Direct imports from `windmill_ai` make ownership and dependency direction clearer at each call site. + +Also do not make `build_proxy_request(raw_body, path)` too narrow. The proxy path needs method, incoming headers, resolved credentials, base URL/platform, organization/user fields, custom headers, and Bedrock/Azure/Vertex-specific context. Introduce a structured `ProxyBuildArgs`/`ProviderCredentials` shape before deleting `AIRequestConfig::prepare_request`, `google.rs`, or `bedrock.rs`. + +## Next Phase PR: Shared Plumbing Only + +Goal: make `windmill-ai` own the provider-independent helper code that later provider moves will need, without changing API proxy behavior or agent request behavior. + +Suggested PR title: `refactor(ai): move shared SSE plumbing into windmill-ai`. + +Scope: +- Add `windmill-ai/src/utils.rs`. +- Move the duplicated `AI_HTTP_HEADERS` parsing into `windmill_ai::utils` with identical parsing behavior. +- Move `extract_text_content` and `should_use_structured_output_tool` from `windmill-worker/src/ai/utils.rs` to `windmill_ai::utils`. +- Move `windmill-worker/src/ai/sse.rs` to `windmill-ai/src/sse.rs`. +- Delete `windmill-worker/src/ai/sse.rs` and update callers to import parser types from `windmill_ai::sse`. +- Update callers of moved utility functions to import from `windmill_ai::utils` directly. +- Add the minimal new `windmill-ai` dependencies required by `sse.rs` (`eventsource-stream`, `tokio-stream`) and avoid adding worker/queue dependencies. + +Out of scope: +- Do not move provider implementations. +- Do not move `image_handler`. +- Do not change `QueryBuilder` method signatures. +- Do not add `build_proxy_request`. +- Do not change API proxy routing, request preparation, credential resolution, audit logging, cache behavior, or Bedrock/Google special cases. +- Do not remove `windmill-api/src/google.rs`, `windmill-api/src/bedrock.rs`, or `AIRequestConfig::prepare_request`. + +Implementation checklist: +1. Add `utils.rs` to `windmill-ai` and export it from `lib.rs`. +2. Move `AI_HTTP_HEADERS` exactly once, then update `windmill-api/src/ai.rs` and `windmill-worker/src/ai_executor.rs` to import it. +3. Move the two provider-independent helper functions into `windmill_ai::utils`; leave worker-specific flow/MCP/conversation utilities in `windmill-worker/src/ai/utils.rs`. +4. Move `sse.rs` into `windmill-ai`, change imports from `crate::ai::{query_builder, types}` to `crate::{query_builder, types}`, and keep behavior unchanged. +5. Remove worker `ai/sse.rs` and update provider imports to use `windmill_ai::sse` directly. +6. Run focused grep checks for duplicate `AI_HTTP_HEADERS`, old local helper definitions, and accidental `windmill_queue`/worker dependencies from `windmill-ai`. +7. Validate with `cargo check -p windmill-ai`, `cargo check -p windmill-worker`, and `cargo check -p windmill-api`. For `bedrock` builds, also check the existing bedrock feature path. + +Review expectations: +- The diff should be mostly moved code and import updates. +- The behavior should be byte-for-byte equivalent where practical. +- Tests are only needed if helper behavior changes. For a pure move, existing backend checks plus manual AI streaming verification are enough. + ## Step-by-Step Plan Each step produces a compiling, working backend. @@ -90,7 +142,7 @@ Move from `windmill-worker/src/ai/sse.rs` to `windmill-ai/src/sse.rs`: --- -### Step 5: Move provider implementations to windmill-ai +### Step 5: Move provider implementations to windmill-ai ✅ Move from `windmill-worker/src/ai/providers/` to `windmill-ai/src/providers/`: - `anthropic.rs` — `AnthropicQueryBuilder` @@ -107,7 +159,7 @@ Move utility functions providers depend on: --- -### Step 6: Move image_handler to windmill-ai +### Step 6: Move image_handler to windmill-ai ✅ Move from `windmill-worker/src/ai/image_handler.rs` to `windmill-ai/src/image_handler.rs`: - `download_and_encode_s3_image` — no signature change needed @@ -131,18 +183,27 @@ This is the key unification step. Add a new method to the `QueryBuilder` trait: /// Used by the API chat proxy. Handles format conversion for non-OpenAI providers. fn build_proxy_request( &self, - raw_body: &[u8], - path: &str, + args: &ProxyBuildArgs<'_>, ) -> Result; ``` -Where `ProxyRequest` contains the transformed body, endpoint URL, and auth headers: +Where `ProxyBuildArgs` carries the API proxy context that provider implementations need: +```rust +pub struct ProxyBuildArgs<'a> { + pub method: http::Method, + pub path: &'a str, + pub headers: &'a http::HeaderMap, + pub body: &'a [u8], + pub credentials: &'a ProviderCredentials, +} +``` + +And `ProxyRequest` contains the transformed request: ```rust pub struct ProxyRequest { pub url: String, pub body: Vec, - pub auth_headers: Vec<(String, String)>, - pub is_sse: bool, + pub headers: Vec<(String, String)>, } ``` @@ -153,9 +214,9 @@ pub struct ProxyRequest { - **Bedrock**: Convert OpenAI format → Bedrock SDK calls. Replaces `windmill-api/src/bedrock.rs`. **Refactor API proxy** (`windmill-api/src/ai.rs`): -1. Parse provider from headers, resolve credentials → `ProviderWithResource` +1. Parse provider from headers, resolve credentials → `ProviderCredentials` 2. Create `QueryBuilder` via `create_query_builder` -3. Call `query_builder.build_proxy_request(body, path)` → `ProxyRequest` +3. Call `query_builder.build_proxy_request(&proxy_args)` → `ProxyRequest` 4. Send the request, return response with SSE keepalive injection **Remove** from windmill-api: @@ -166,7 +227,7 @@ pub struct ProxyRequest { - `supports_native_fim`, `transform_fim_to_chat_completions` — moved to windmill-ai **Keep** in API: -- `AIRequestConfig::new` credential resolution (or refactor to produce `ProviderWithResource`) +- `AIRequestConfig::new` credential resolution until it is refactored to produce `ProviderCredentials` - HTTP routes, audit logging, request caching - `inject_keepalives`, `is_sse_response` helpers - `AIConfig`, `ExpiringAIRequestConfig` caching types diff --git a/flake.nix b/flake.nix index aaf06c882e..3872ee636f 100644 --- a/flake.nix +++ b/flake.nix @@ -79,11 +79,11 @@ # --------------------------------------------------------------- rustyV8Archive = let - version = "130.0.7"; + version = "137.1.0"; target = stdenv.hostPlatform.rust.rustcTarget; sha256 = { x86_64-linux = - "sha256-pkdsuU6bAkcIHEZUJOt5PXdzK424CEgTLXjLtQ80t10="; + "sha256-Tiscfy2bzYGR3s0T+SC1IB3xWvTVpVcSEdjq3MCRoRw="; aarch64-linux = lib.fakeHash; x86_64-darwin = lib.fakeHash; aarch64-darwin = lib.fakeHash; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2e8ca86313..2483fa4d97 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-components", - "version": "1.697.0", + "version": "1.700.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-components", - "version": "1.697.0", + "version": "1.700.2", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/frontend/package.json b/frontend/package.json index 79d4465429..32bdc019f0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "windmill-components", - "version": "1.697.0", + "version": "1.700.2", "scripts": { "dev": "vite dev", "build": "vite build", diff --git a/frontend/src/lib/components/Auth0Setting.svelte b/frontend/src/lib/components/Auth0Setting.svelte index beb9ec03f9..a246ae196d 100644 --- a/frontend/src/lib/components/Auth0Setting.svelte +++ b/frontend/src/lib/components/Auth0Setting.svelte @@ -7,6 +7,7 @@ import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte' import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte' import TextInput from './text_input/TextInput.svelte' + import Password from './Password.svelte' import SettingCard from './instanceSettings/SettingCard.svelte' interface Props { @@ -103,14 +104,15 @@ class="max-w-lg" /> - - -