mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-12 16:02:56 +00:00
Merge origin/main into fix-runnable-test-draft-app
Seven conflicts, all in the app-write path and all the same shape: main added a
code diff to each app tool's result (`finishAppDraftWrite` grew a `codeDiff`
argument, and the file tools capture the before-text), while this branch changed
those same calls to address a resolved target and report the name the app deploys
under. Both kept: the diff argument and `inlineRunnableDiff` from main, the
resolved target and `appPath` closure from here. Main's side of each hunk still
called `loadAppDraftValue(args.path, …)` and read `{ value }` from it, which no
longer type-checks — the merged helpers take a target and return the value — so
the call shape had to come from this side.
The last conflict is the ACTIVE EDITOR block: main added an `isNew` hint telling
the model not to create a second item over a template, this branch emits the
storage key as `path` with the chosen name as `draft_path`. Both are now emitted.
Main also added `quickjs-emscripten-core`, so this worktree needed `npm install`;
without it `tsc` reports the module missing.
One test, `test_run_step opens an aiagent step on the step's own configuration`,
hangs after this merge. It hangs the same way on pristine `origin/main`
(`eab3dd5a52`, 30s with the timeout raised), so it arrived with main rather than
with this resolution — a seventh pre-existing failure, not a regression.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,25 +1,22 @@
|
||||
name: AI Evals (scheduled)
|
||||
|
||||
# Full ai_evals suites on current models, posted to the AI evals dashboard in
|
||||
# the windmill-prod workspace (f/ai/ai_evals_dashboard) so quality is tracked
|
||||
# over time. Weekly, since one pass of every suite at --runs 3 costs about 50M
|
||||
# tokens per model: every suite runs on MODELS, and global (the mode users get)
|
||||
# also runs on GLOBAL_EXTRA_MODELS, one flagship per other provider. Run it by
|
||||
# hand to measure a branch against main.
|
||||
# The global (AI chat) and cli suites on current models, posted to the AI evals
|
||||
# dashboard in the windmill-prod workspace (f/ai/ai_evals_dashboard) so quality is
|
||||
# tracked over time. Weekly: one global pass at --runs 3 is about 45M tokens per
|
||||
# model. Each mode and model is its own job, so a slow model neither delays nor
|
||||
# shares a timeout with the others. Run it by hand to measure a branch against
|
||||
# main, or to include the flow, app and script suites.
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 3 * * 1"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
modes:
|
||||
description: "Space-separated modes"
|
||||
default: "global flow app script cli"
|
||||
description: "Space-separated modes (global, cli, flow, app, script)"
|
||||
default: "global cli"
|
||||
models:
|
||||
description: "Space-separated model aliases (bun run cli -- models)"
|
||||
default: "sonnet-5.5"
|
||||
global_extra_models:
|
||||
description: "Extra model aliases for global mode only"
|
||||
default: "gpt-6-astra gemini-3.8-flash"
|
||||
description: "Space-separated model aliases (bun run cli -- models); cli runs only the Claude ones"
|
||||
default: "sonnet-5.5 opus-5.5 gpt-6-sol gpt-6-astra deepseek-v4-pro"
|
||||
runs:
|
||||
description: "Runs per case"
|
||||
default: "3"
|
||||
@@ -31,24 +28,30 @@ concurrency:
|
||||
group: ai-evals-scheduled-${{ github.ref }}
|
||||
|
||||
env:
|
||||
MODELS: ${{ inputs.models || 'sonnet-5.5' }}
|
||||
GLOBAL_EXTRA_MODELS: ${{ inputs.global_extra_models || 'gpt-6-astra gemini-3.8-flash' }}
|
||||
MODELS: ${{ inputs.models || 'sonnet-5.5 opus-5.5 gpt-6-sol gpt-6-astra deepseek-v4-pro' }}
|
||||
RUNS: ${{ inputs.runs || '3' }}
|
||||
REASONING: ${{ inputs.reasoning }}
|
||||
INGEST_URL: https://app.windmill.dev/api/r/f/ai/ingest_ai_eval_run
|
||||
# Routes on the cloud instance are prefixed with their workspace.
|
||||
INGEST_URL: https://app.windmill.dev/api/r/windmill-prod/f/ai/ingest_ai_eval_run
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
modes: ${{ steps.modes.outputs.modes }}
|
||||
matrix: ${{ steps.matrix.outputs.matrix }}
|
||||
steps:
|
||||
- id: modes
|
||||
- id: matrix
|
||||
env:
|
||||
MODES: ${{ inputs.modes || 'global flow app script cli' }}
|
||||
MODES: ${{ inputs.modes || 'global cli' }}
|
||||
# cli drives the Claude Agent SDK, so it only takes the Claude aliases
|
||||
# (`cli` entries in ai_evals/core/models.ts).
|
||||
run: |
|
||||
echo "modes=$(jq -cn --arg m "$MODES" '$m | split(" ")
|
||||
| map(select(IN("global", "flow", "app", "script", "cli")))')" >> "$GITHUB_OUTPUT"
|
||||
echo "matrix=$(jq -cn --arg modes "$MODES" --arg models "$MODELS" '
|
||||
def words: split(" ") | map(select(. != ""));
|
||||
[ $modes | words[] | select(IN("global", "flow", "app", "script", "cli")) as $mode
|
||||
| $models | words[]
|
||||
| select($mode != "cli" or test("^(claude-)?(haiku|sonnet|opus)"; "i"))
|
||||
| {mode: $mode, model: .} ]')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
evals:
|
||||
needs: setup
|
||||
@@ -57,7 +60,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
mode: ${{ fromJSON(needs.setup.outputs.modes) }}
|
||||
include: ${{ fromJSON(needs.setup.outputs.matrix) }}
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
@@ -117,10 +120,11 @@ jobs:
|
||||
working-directory: ./cli
|
||||
run: bun install && ./gen_wm_client.sh && ./windmill-utils-internal/gen_wm_client.sh
|
||||
|
||||
- name: Run ${{ matrix.mode }} evals
|
||||
- name: Run ${{ matrix.mode }} evals on ${{ matrix.model }}
|
||||
working-directory: ./ai_evals
|
||||
env:
|
||||
MODE: ${{ matrix.mode }}
|
||||
MODEL: ${{ matrix.model }}
|
||||
WMILL_AI_EVAL_BACKEND_URL: http://localhost:8000
|
||||
WMILL_AI_EVAL_BACKEND_WORKSPACE: integration-tests
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
@@ -130,14 +134,10 @@ jobs:
|
||||
run: |
|
||||
bun install
|
||||
mkdir -p results
|
||||
models="$MODELS"
|
||||
[ "$MODE" = global ] && models="$models $GLOBAL_EXTRA_MODELS"
|
||||
reasoning=()
|
||||
[ -n "$REASONING" ] && [ "$MODE" != cli ] && reasoning=(--reasoning "$REASONING")
|
||||
for m in $models; do
|
||||
bun run cli -- run "$MODE" --model "$m" --runs "$RUNS" "${reasoning[@]}" \
|
||||
--output "$PWD/results/$MODE-$m.json" || echo "::warning::$MODE on $m errored"
|
||||
done
|
||||
bun run cli -- run "$MODE" --model "$MODEL" --runs "$RUNS" "${reasoning[@]}" \
|
||||
--output "$PWD/results/$MODE-$MODEL.json" || echo "::warning::$MODE on $MODEL errored"
|
||||
|
||||
- name: Post results to the dashboard
|
||||
if: always()
|
||||
@@ -156,7 +156,7 @@ jobs:
|
||||
| .cases[].attempts[] |= {attempt, passed, durationMs, toolCallCount, judgeScore,
|
||||
judgeSummary, error, tokenUsage, checks: [.checks[]? | {name, passed}]}),
|
||||
git_ref: $ref, trigger: $trigger, run_url: $url}' "$f" \
|
||||
| curl -sf --retry 3 -X POST "$INGEST_URL" \
|
||||
| curl -sS --fail-with-body --retry 3 -X POST "$INGEST_URL" \
|
||||
-H "Authorization: Bearer $INGEST_TOKEN" -H 'content-type: application/json' \
|
||||
--data-binary @- && echo " <- $f" || echo "::warning::failed to post $f"
|
||||
done
|
||||
@@ -165,7 +165,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: ai-evals-${{ matrix.mode }}
|
||||
name: ai-evals-${{ matrix.mode }}-${{ matrix.model }}
|
||||
path: |
|
||||
ai_evals/logs
|
||||
ai_evals/results
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "1.820.0"
|
||||
".": "1.825.0"
|
||||
}
|
||||
|
||||
@@ -90,6 +90,12 @@ are for a plain checkout with nothing running.
|
||||
- **Login**: `admin@windmill.dev` / `changeme`
|
||||
- **Instance settings**: navigate to `/#superadmin-settings`
|
||||
- **Migrations**: use `cargo sqlx migrate add -r <name>` from `backend/` to create new migrations (never generate timestamps manually)
|
||||
- **Resource types ship through the hub, not migrations.** A new type, a provider's credential
|
||||
included, is a `hub/<app>/<app>.resource_type.json` in `windmill-labs/windmill-integrations`
|
||||
and reaches instances by hub sync; a product name its identifier misses (`typesafe` →
|
||||
"TypeSafe") is that file's `display_name`, not a word in `resourceTypeDisplay.ts`'s map. Only a
|
||||
type Windmill itself defines and reads as a product object (`ai_skill`, `ai_instruction`) is
|
||||
also seeded by a migration, so the feature works before a sync. For local testing, insert the row.
|
||||
|
||||
### Per-worktree ports and database
|
||||
|
||||
|
||||
+145
@@ -1,5 +1,150 @@
|
||||
# Changelog
|
||||
|
||||
## [1.825.0](https://github.com/windmill-labs/windmill/compare/v1.824.1...v1.825.0) (2026-10-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* choose build with AI or code when creating a raw app ([#11560](https://github.com/windmill-labs/windmill/issues/11560)) ([431f861](https://github.com/windmill-labs/windmill/commit/431f86138815fce432e282e8ac495953c5ecf98a))
|
||||
* confine jobs:read to the runnable paths it names ([#11550](https://github.com/windmill-labs/windmill/issues/11550)) ([b371af3](https://github.com/windmill-labs/windmill/commit/b371af31c96e6890fe9223233d2c6e53ddea67d4))
|
||||
* open a run as a pushed page on the narrow runs page ([#11541](https://github.com/windmill-labs/windmill/issues/11541)) ([fd8de45](https://github.com/windmill-labs/windmill/commit/fd8de45cc7cfcba2c374dd86f87f4d3e8aec6ded))
|
||||
* say a canceled team plan is canceled, not past due ([#11539](https://github.com/windmill-labs/windmill/issues/11539)) ([18e44d3](https://github.com/windmill-labs/windmill/commit/18e44d31742b8d16852b8c6ae663979666e064d7))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* group raw app edits that carry a file diff in the ai chat ([#11558](https://github.com/windmill-labs/windmill/issues/11558)) ([5912d30](https://github.com/windmill-labs/windmill/commit/5912d300a550a33674fd73740d613d1b434fbba1))
|
||||
* honor custom timeout in setup phases and name the limit hit ([#11557](https://github.com/windmill-labs/windmill/issues/11557)) ([4fd4f76](https://github.com/windmill-labs/windmill/commit/4fd4f76d0df740cb3eebed1a0625b970484807fc))
|
||||
* search slow runs filters by time windows instead of timing out ([#11549](https://github.com/windmill-labs/windmill/issues/11549)) ([2bf9cfc](https://github.com/windmill-labs/windmill/commit/2bf9cfcd59e1101695160524c31a4da1a41699dc))
|
||||
* serve the raw-app preview shell sandboxed, load a blob copy in the editor ([#11554](https://github.com/windmill-labs/windmill/issues/11554)) ([5f92daa](https://github.com/windmill-labs/windmill/commit/5f92daab6d0ecac30dd384e1b0f302af13fdd9ab))
|
||||
* simplify the run page AI card to a prompt and a chat button ([#11561](https://github.com/windmill-labs/windmill/issues/11561)) ([8c74f75](https://github.com/windmill-labs/windmill/commit/8c74f75de3fa62b07186ca4f56dae4a7e4758034))
|
||||
|
||||
## [1.824.1](https://github.com/windmill-labs/windmill/compare/v1.824.0...v1.824.1) (2026-10-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* keep monaco's diff engine out of the app shell bundle ([#11542](https://github.com/windmill-labs/windmill/issues/11542)) ([0908dee](https://github.com/windmill-labs/windmill/commit/0908dee897e0f87b97418a23ede3662d96270340))
|
||||
|
||||
## [1.824.0](https://github.com/windmill-labs/windmill/compare/v1.823.1...v1.824.0) (2026-10-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add an AI decision flow step (TypeSafe's Jev, Cloudflare's Clef) ([#11474](https://github.com/windmill-labs/windmill/issues/11474)) ([3cba8a7](https://github.com/windmill-labs/windmill/commit/3cba8a7e4cca3ef20ba73a7dad79d892bdc9fed8))
|
||||
* add notion oauth provider ([#11528](https://github.com/windmill-labs/windmill/issues/11528)) ([8b2dc1e](https://github.com/windmill-labs/windmill/commit/8b2dc1e46c7f4e3859263f18b686ce6f3ce6fd30))
|
||||
* **ai-chat:** show per-file diffs for raw app edits ([#11521](https://github.com/windmill-labs/windmill/issues/11521)) ([ec0883a](https://github.com/windmill-labs/windmill/commit/ec0883a627b06b6ba2a393d5cf66ddf6ebc4cd0d))
|
||||
* bill team-plan service accounts as half a seat ([#11298](https://github.com/windmill-labs/windmill/issues/11298)) ([3aee181](https://github.com/windmill-labs/windmill/commit/3aee1815746f0116f5662f162ad1e234717af90d))
|
||||
* card layout and load-more for the audit logs page ([#11507](https://github.com/windmill-labs/windmill/issues/11507)) ([af1a29a](https://github.com/windmill-labs/windmill/commit/af1a29a3cb33386f61a759ec0dd5eec0ccefefc2))
|
||||
* offer audit-log and drive-metadata scopes for google oauth connects ([#11527](https://github.com/windmill-labs/windmill/issues/11527)) ([c4e7c71](https://github.com/windmill-labs/windmill/commit/c4e7c714e455f958d9a927c4bc4eb23256324780))
|
||||
* offer slack read scopes for files, user groups, emoji and dms ([#11526](https://github.com/windmill-labs/windmill/issues/11526)) ([b5fda7d](https://github.com/windmill-labs/windmill/commit/b5fda7d5af7105fde69beb68410984350af2ac7e))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bound s3 ai attachment downloads before encoding ([#11534](https://github.com/windmill-labs/windmill/issues/11534)) ([92fbfc0](https://github.com/windmill-labs/windmill/commit/92fbfc03d219544c16e8f7db47a547f9ff402c27))
|
||||
* collapse queued and running chat tool calls into a waiting row ([#11523](https://github.com/windmill-labs/windmill/issues/11523)) ([4f30982](https://github.com/windmill-labs/windmill/commit/4f30982ce3f7abc64a31890f8e88c31c1c2e0803))
|
||||
* disable postgres triggers whose replication slot was invalidated ([#11536](https://github.com/windmill-labs/windmill/issues/11536)) ([85248ee](https://github.com/windmill-labs/windmill/commit/85248eee786b633def40c42b6007fc7b907fb09e))
|
||||
* evaluate field conditions and step test inputs in QuickJS ([#11487](https://github.com/windmill-labs/windmill/issues/11487)) ([c962d37](https://github.com/windmill-labs/windmill/commit/c962d3766b1a250ba960e413556890d5919ebc94))
|
||||
* flush proxied responses before the dev supervisor closes the client ([#11530](https://github.com/windmill-labs/windmill/issues/11530)) ([cefb195](https://github.com/windmill-labs/windmill/commit/cefb1953764e714d79556e2f8065cfbec78acb86))
|
||||
* label the default slack scopes and list them first in the connect dialog ([#11529](https://github.com/windmill-labs/windmill/issues/11529)) ([e76781c](https://github.com/windmill-labs/windmill/commit/e76781cc2709fda08403e14c1a0a01957e430db1))
|
||||
* run each ai eval model in its own job and let the docs tools reach the backend ([#11533](https://github.com/windmill-labs/windmill/issues/11533)) ([222dad1](https://github.com/windmill-labs/windmill/commit/222dad1d8cc9cbb6925fdff709e00a49bb63bc4a))
|
||||
* show the flow row edit button to builder operators ([#11535](https://github.com/windmill-labs/windmill/issues/11535)) ([6c4e34c](https://github.com/windmill-labs/windmill/commit/6c4e34cd0dd9b4329084214c40695f5915947cc2))
|
||||
|
||||
## [1.823.1](https://github.com/windmill-labs/windmill/compare/v1.823.0...v1.823.1) (2026-10-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* python results with a KeyError-raising __getattr__ fail the job ([#11517](https://github.com/windmill-labs/windmill/issues/11517)) ([3fff1bd](https://github.com/windmill-labs/windmill/commit/3fff1bd3dcedce22c58d988ad2e725e3d4e96e0e))
|
||||
* stream the ai chat compaction request ([#11514](https://github.com/windmill-labs/windmill/issues/11514)) ([289d3c3](https://github.com/windmill-labs/windmill/commit/289d3c3941c2d9997bf0476bf9bfdfd948542c55))
|
||||
|
||||
## [1.823.0](https://github.com/windmill-labs/windmill/compare/v1.822.0...v1.823.0) (2026-10-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* redesign the service logs page ([#11506](https://github.com/windmill-labs/windmill/issues/11506)) ([0f65df2](https://github.com/windmill-labs/windmill/commit/0f65df24d2cabd9d8daa71c103969e6392e37b4f))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* apply the pg keepalive settings to the postgres trigger connection ([#11512](https://github.com/windmill-labs/windmill/issues/11512)) ([33f6de0](https://github.com/windmill-labs/windmill/commit/33f6de0cd20f45ff56770f946a4731e5b13bf677))
|
||||
* bound the datatable migration lock wait and reap a dead holder ([#11510](https://github.com/windmill-labs/windmill/issues/11510)) ([711e08c](https://github.com/windmill-labs/windmill/commit/711e08c1d20a326142235c83d8805b7990bf6e6d))
|
||||
* bound the pg cached-connection reset probe so a vanished server fails fast ([#11505](https://github.com/windmill-labs/windmill/issues/11505)) ([d4a423f](https://github.com/windmill-labs/windmill/commit/d4a423f567e1fb5e21016a2be61d858ab61a4d4f))
|
||||
* cancel an abandoned pg query and close failed connections cleanly ([#11511](https://github.com/windmill-labs/windmill/issues/11511)) ([26856b8](https://github.com/windmill-labs/windmill/commit/26856b8061c28eade191c7d85a32b74ca44c4fea))
|
||||
* delete a flow chat turn with its flow run, not its step jobs ([#11477](https://github.com/windmill-labs/windmill/issues/11477)) ([35ec16d](https://github.com/windmill-labs/windmill/commit/35ec16d12728531ab90632116913e98f80d1cc05))
|
||||
* drop a fork's datatable database despite connected sessions ([#11509](https://github.com/windmill-labs/windmill/issues/11509)) ([fa419af](https://github.com/windmill-labs/windmill/commit/fa419af9b3f7d65a607bb8ea2bcea58223f743c4))
|
||||
|
||||
## [1.822.0](https://github.com/windmill-labs/windmill/compare/v1.821.0...v1.822.0) (2026-10-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add a cancel-only jobs:cancel token scope, optionally path-scoped ([#11479](https://github.com/windmill-labs/windmill/issues/11479)) ([4bc7e0d](https://github.com/windmill-labs/windmill/commit/4bc7e0d7bad22e9e1a3532ca747ed863f90213ba))
|
||||
* add an instance setting routing all dependency jobs to one tag ([#11486](https://github.com/windmill-labs/windmill/issues/11486)) ([3d1d249](https://github.com/windmill-labs/windmill/commit/3d1d2495567274b4c48babb374e7716c4ed24ecc))
|
||||
* add trigger, tag, run-as and digest claims to job OIDC tokens ([#11481](https://github.com/windmill-labs/windmill/issues/11481)) ([4beb1f9](https://github.com/windmill-labs/windmill/commit/4beb1f9420524425c354e82729e4e35c5bf65451))
|
||||
* log pg job progress, warn on stalls, detect dead db connections ([#11491](https://github.com/windmill-labs/windmill/issues/11491)) ([d722772](https://github.com/windmill-labs/windmill/commit/d7227726be2b082bbfb204abb75a3ff081db77de))
|
||||
* read the OIDC signing key from a file and rotate it ([#11482](https://github.com/windmill-labs/windmill/issues/11482)) ([d949484](https://github.com/windmill-labs/windmill/commit/d949484e7ba9911cf128387e096f78c917fb3f35))
|
||||
* replace the AI input filling toggle with an additional prompt ([#11501](https://github.com/windmill-labs/windmill/issues/11501)) ([e952298](https://github.com/windmill-labs/windmill/commit/e952298f84d04cbb11f0c44c32435f0b711bf3bf))
|
||||
* restricted job tokens per script and flow ([#11484](https://github.com/windmill-labs/windmill/issues/11484)) ([e7fc1b2](https://github.com/windmill-labs/windmill/commit/e7fc1b2e2e9b358b414d0ca0eaf2753e142dded7))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* keep a resource default set in the schema when the script is redeployed ([#11495](https://github.com/windmill-labs/windmill/issues/11495)) ([68f8f19](https://github.com/windmill-labs/windmill/commit/68f8f19b33768451ebab267b36f931a6f386c678))
|
||||
* let instances withhold signing secrets from the settings API ([#11483](https://github.com/windmill-labs/windmill/issues/11483)) ([99e96d3](https://github.com/windmill-labs/windmill/commit/99e96d3f781decda8fed2cbd5409cbc5c3520eaa))
|
||||
* let legacy draft-only items be discarded from the home page ([#11488](https://github.com/windmill-labs/windmill/issues/11488)) ([98274a7](https://github.com/windmill-labs/windmill/commit/98274a73361618ff37a929584e5b5f519b71a912))
|
||||
* never reuse a pg connection a script left inside a transaction ([#11497](https://github.com/windmill-labs/windmill/issues/11497)) ([b7f74cd](https://github.com/windmill-labs/windmill/commit/b7f74cd56294cdb7052e311c5a68de9ee0bb4122))
|
||||
* read inet and cidr results with their prefix, including arrays ([#11476](https://github.com/windmill-labs/windmill/issues/11476)) ([ae093b7](https://github.com/windmill-labs/windmill/commit/ae093b7666cefc3777be0e45144bb272732846d0))
|
||||
* stop the pg executor cache from pinning a pooler slot ([#11490](https://github.com/windmill-labs/windmill/issues/11490)) ([eed7e7d](https://github.com/windmill-labs/windmill/commit/eed7e7d9e6bb375daebbb323b1288c36a278f977))
|
||||
* unstick postgres jobs on large results with custom-typed columns ([#11475](https://github.com/windmill-labs/windmill/issues/11475)) ([14801fd](https://github.com/windmill-labs/windmill/commit/14801fdd68407add824bfa2c679c0e8cb4788610))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* keep a pg executor connection per database ([#11492](https://github.com/windmill-labs/windmill/issues/11492)) ([013b302](https://github.com/windmill-labs/windmill/commit/013b302d484b967443cbf747fb9f2ecbb1145033))
|
||||
|
||||
## [1.821.0](https://github.com/windmill-labs/windmill/compare/v1.820.0...v1.821.0) (2026-10-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add //no_network annotation for native scripts ([#11445](https://github.com/windmill-labs/windmill/issues/11445)) ([c25bb1e](https://github.com/windmill-labs/windmill/commit/c25bb1ed90d88167df0c36817942427a32fc3205))
|
||||
* add alert actions and reduce alert padding ([#11439](https://github.com/windmill-labs/windmill/issues/11439)) ([52c3188](https://github.com/windmill-labs/windmill/commit/52c31881fc433a03ef13f7a8cde1410da8634e19))
|
||||
* add test key button to AI resource drawers ([#11466](https://github.com/windmill-labs/windmill/issues/11466)) ([26be1d7](https://github.com/windmill-labs/windmill/commit/26be1d7da8be48c2e7d7a6b95d406513d649be4e))
|
||||
* add wmill datatable migrate status ([#11465](https://github.com/windmill-labs/windmill/issues/11465)) ([7f1bead](https://github.com/windmill-labs/windmill/commit/7f1beadf793e8db05333d253f464954510961a5b))
|
||||
* agents as a standalone kind with home listing, detail and editor pages ([#11332](https://github.com/windmill-labs/windmill/issues/11332)) ([8c7dcbb](https://github.com/windmill-labs/windmill/commit/8c7dcbbda58c86743bf3822c422be7faf1e8df51))
|
||||
* dispatch ai agent tools through the worker queue ([#11227](https://github.com/windmill-labs/windmill/issues/11227)) ([039f49e](https://github.com/windmill-labs/windmill/commit/039f49ef124eacefc0ee34dcb011ea5773383845))
|
||||
* group consecutive tool calls in the AI chat ([#11329](https://github.com/windmill-labs/windmill/issues/11329)) ([9613549](https://github.com/windmill-labs/windmill/commit/9613549abc1c1d63abb252e8a675c6a73142b26f))
|
||||
* harden job OIDC tokens with version claims, jti and a lifetime cap ([#11457](https://github.com/windmill-labs/windmill/issues/11457)) ([227e934](https://github.com/windmill-labs/windmill/commit/227e9342108bf7c5be5721a777ff9ec544a44f08))
|
||||
* labelled scope checkboxes in the oauth connect dialog ([#11460](https://github.com/windmill-labs/windmill/issues/11460)) ([840567f](https://github.com/windmill-labs/windmill/commit/840567fbe5c8779ac163b02a6e02e3d8ec1178a8))
|
||||
* let operators compose flows when the workspace grants the right ([#11228](https://github.com/windmill-labs/windmill/issues/11228)) ([ca44043](https://github.com/windmill-labs/windmill/commit/ca44043e12d4713d67ca13eb546eef9d00cc965e))
|
||||
* let slack connects issue a user token via user_scope ([#11452](https://github.com/windmill-labs/windmill/issues/11452)) ([d518aa5](https://github.com/windmill-labs/windmill/commit/d518aa5557910d10ec5a8ca6fff3fd0ad2fa31fc))
|
||||
* make hub integrations usable as examples in global AI chat ([#10599](https://github.com/windmill-labs/windmill/issues/10599)) ([8953ca6](https://github.com/windmill-labs/windmill/commit/8953ca670aac63feab1a04369df50a669c1f40bc))
|
||||
* offer slack write scopes as bot and user scope options ([#11472](https://github.com/windmill-labs/windmill/issues/11472)) ([ab2de83](https://github.com/windmill-labs/windmill/commit/ab2de838d76e2229da0867c93c2f18dc34d0d076))
|
||||
* open chat path pill actions from a hover menu ([#11441](https://github.com/windmill-labs/windmill/issues/11441)) ([083db5e](https://github.com/windmill-labs/windmill/commit/083db5e38855586491920abf678e3247d8c82a43))
|
||||
* rename agent memory options to On and Legacy, explain each ([#11442](https://github.com/windmill-labs/windmill/issues/11442)) ([077709b](https://github.com/windmill-labs/windmill/commit/077709b914b8b58cbd8c99f1048c7fecc2978c6b))
|
||||
* **sessions:** show an item's deployed page in the preview panel ([#11458](https://github.com/windmill-labs/windmill/issues/11458)) ([b10f83c](https://github.com/windmill-labs/windmill/commit/b10f83c763abf49118c05dc08ec2b19570e21475))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **apps:** hide custom component host while it is not rendered ([#11434](https://github.com/windmill-labs/windmill/issues/11434)) ([c728346](https://github.com/windmill-labs/windmill/commit/c728346a151e4a0a2d1ecaae397068871186323d))
|
||||
* await async def main in python scripts ([#11437](https://github.com/windmill-labs/windmill/issues/11437)) ([94ac34a](https://github.com/windmill-labs/windmill/commit/94ac34a4891a8572ad2493bdbbbf2f4911236aff))
|
||||
* block runs while a JSON input does not parse ([#11463](https://github.com/windmill-labs/windmill/issues/11463)) ([ddde41b](https://github.com/windmill-labs/windmill/commit/ddde41bc1de6f7dea32b21a61ce2a5bf3bf8bba6))
|
||||
* bump git sync hub scripts to cli 1.820.1 ([#11438](https://github.com/windmill-labs/windmill/issues/11438)) ([7664001](https://github.com/windmill-labs/windmill/commit/7664001b36afc80bd8d26cb12c917c971e24e96f))
|
||||
* **cli:** resolve fork specific items from the parent workspace entry ([#11436](https://github.com/windmill-labs/windmill/issues/11436)) ([1d8d4a1](https://github.com/windmill-labs/windmill/commit/1d8d4a16b4396acb70484c858058353eb1ba9d6f))
|
||||
* collapse run card in ai chat when the run is declined ([#11435](https://github.com/windmill-labs/windmill/issues/11435)) ([ab2a973](https://github.com/windmill-labs/windmill/commit/ab2a973357524c2c3aa81cc4aeb716d8cfd108d1))
|
||||
* embed a VERSIONINFO resource in the Windows binary ([#11433](https://github.com/windmill-labs/windmill/issues/11433)) ([9028432](https://github.com/windmill-labs/windmill/commit/9028432e8a71b9601ff65160a29e683a9cc24989))
|
||||
* ignore edited_at and edited_by in flow and item diffs ([#11455](https://github.com/windmill-labs/windmill/issues/11455)) ([851f6d7](https://github.com/windmill-labs/windmill/commit/851f6d74c98e849b25d6999e3d8ee492c997d5a9))
|
||||
* keep flow graph rendered between deploy and navigation ([#11454](https://github.com/windmill-labs/windmill/issues/11454)) ([c0fb2db](https://github.com/windmill-labs/windmill/commit/c0fb2db24454d17d33fffa41b59e6a4cf61db44f))
|
||||
* lost suspend decrement when concurrent approvals resume a flow ([#11450](https://github.com/windmill-labs/windmill/issues/11450)) ([a8e65ab](https://github.com/windmill-labs/windmill/commit/a8e65ab1e6287805f616f2531dc6a62134134e36))
|
||||
* **npm-proxy:** support _auth and username/_password basic auth from npmrc ([#11467](https://github.com/windmill-labs/windmill/issues/11467)) ([1c6758a](https://github.com/windmill-labs/windmill/commit/1c6758a65651d90088f981317c06b12a19f3b73f))
|
||||
* offer user scopes in the slack scope editor and drop the generated description on type change ([#11464](https://github.com/windmill-labs/windmill/issues/11464)) ([153b02a](https://github.com/windmill-labs/windmill/commit/153b02a5de19c615e6640d52ad48b1c507402851))
|
||||
* restore the save to workspace button on inline flow steps ([#11469](https://github.com/windmill-labs/windmill/issues/11469)) ([88d0cd0](https://github.com/windmill-labs/windmill/commit/88d0cd00e54de7dd9b008f6f233992cb6ae69571))
|
||||
* stop running deleted script versions from worker caches ([#11456](https://github.com/windmill-labs/windmill/issues/11456)) ([d2830cb](https://github.com/windmill-labs/windmill/commit/d2830cb4a0effc369aa5dbebdb32e770a2c87e18))
|
||||
|
||||
## [1.820.0](https://github.com/windmill-labs/windmill/compare/v1.819.0...v1.820.0) (2026-09-29)
|
||||
|
||||
|
||||
|
||||
+3
-3
@@ -142,10 +142,10 @@ FROM ${DEBIAN_IMAGE}
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG POWERSHELL_VERSION=7.5.0
|
||||
ARG KUBECTL_VERSION=1.36.2
|
||||
ARG KUBECTL_VERSION=1.37.1
|
||||
ARG HELM_VERSION=3.21.4
|
||||
# NOTE: If changing, also change go version in workspace dependencies template at WorkspaceDependenciesEditor.svelte
|
||||
ARG GO_VERSION=1.26.8
|
||||
ARG GO_VERSION=1.27.1
|
||||
ARG APP=/usr/src/app
|
||||
ARG WITH_POWERSHELL=true
|
||||
ARG WITH_KUBECTL=true
|
||||
@@ -318,7 +318,7 @@ COPY --from=nsjail /nsjail/nsjail /bin/nsjail
|
||||
|
||||
# crane: pulls + flattens images for the sandboxed container runtime (`# sandbox <image>`).
|
||||
# Single static binary — no daemon/store/root needed. See docs/docker-v2-runtime.md.
|
||||
ARG CRANE_VERSION=v0.21.7
|
||||
ARG CRANE_VERSION=v0.22.1
|
||||
RUN arch="$(dpkg --print-architecture)"; \
|
||||
case "$arch" in amd64) crane_arch=x86_64 ;; arm64) crane_arch=arm64 ;; *) echo >&2 "error: unsupported arch '$arch' for crane"; exit 1 ;; esac; \
|
||||
wget -O /tmp/crane.tgz "https://github.com/google/go-containerregistry/releases/download/${CRANE_VERSION}/go-containerregistry_Linux_${crane_arch}.tar.gz" \
|
||||
|
||||
@@ -6,6 +6,8 @@ import { dirname, resolve } from 'path'
|
||||
// @ts-ignore - Node.js url
|
||||
import { fileURLToPath } from 'url'
|
||||
import { handleBenchmarkApiFetch, hasBenchmarkApiHandler } from './mockBackend'
|
||||
import { resolveWindmillBackendSettings } from '../../core/windmillBackendSettings'
|
||||
import { WindmillBackendClient } from './windmillBackend'
|
||||
|
||||
const FRONTEND_DIR = fileURLToPath(new URL('../../../frontend/', import.meta.url))
|
||||
|
||||
@@ -28,6 +30,11 @@ globalThis.fetch = (async (input: unknown, init?: RequestInit) => {
|
||||
if (typeof url === 'string' && hasBenchmarkApiHandler(url)) {
|
||||
return handleBenchmarkApiFetch(url, init)
|
||||
}
|
||||
// The docs tools read the documentation bundled into the backend: hand them to the
|
||||
// benchmark's real backend, logged in like the AI proxy calls.
|
||||
if (typeof url === 'string' && url.startsWith('/api/docs/')) {
|
||||
return new WindmillBackendClient(resolveWindmillBackendSettings()).request(url.slice('/api'.length), init)
|
||||
}
|
||||
// The parsers behind inferArgs load their wasm from a vite `?url` path, which only a dev
|
||||
// server serves. Unserved, every script schema infers as empty, and the tools then tell the
|
||||
// model its arguments are undeclared, so it rewrites a correct script until it runs out of turns.
|
||||
@@ -684,6 +691,7 @@ benchmarkIt(
|
||||
resetBenchmarkMockBackend()
|
||||
}
|
||||
},
|
||||
// Full-suite runs (30+ cases at concurrency 2-3) routinely exceed 10 minutes.
|
||||
7_200_000
|
||||
// A full suite on a slow reasoning model runs for hours (gpt-6-astra on global went past
|
||||
// 2h); the CI job's own timeout is the real bound.
|
||||
18_000_000
|
||||
)
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO script\n (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, cache_ignore_s3_path, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, delete_after_secs, timeout, concurrency_key, visible_to_runner_only, auto_kind, codebase, has_preprocessor, on_behalf_of, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, runnable_settings_handle, modules, labels, job_token_scopes, lock_error_logs, created_at)\n\n SELECT workspace_id, $1, path, array_prepend($2::bigint, COALESCE(parent_hashes, '{}'::bigint[])), summary, description, content, created_by, schema, is_template, extra_perms, $4::text, language, kind, tag, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, cache_ignore_s3_path, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, delete_after_secs, timeout, concurrency_key, visible_to_runner_only, auto_kind, codebase, has_preprocessor, on_behalf_of, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, runnable_settings_handle, COALESCE($5::jsonb, modules), labels, job_token_scopes, $6::text, clock_timestamp()\n\n FROM script WHERE hash = $2 AND workspace_id = $3;\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Text",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "05b610ad1809d487c32e85b0392cfca8913ba1129fdf4978a8a4738689786160"
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (\n id, -- 1\n workspace_id, -- 2\n raw_code, -- 3\n raw_lock, -- 4\n raw_flow, -- 5\n tag, -- 6\n parent_job, -- 7\n created_by, -- 8\n permissioned_as, -- 9\n runnable_id, -- 10\n runnable_path, -- 11\n args, -- 12\n kind, -- 13\n trigger, -- 14\n script_lang, -- 15\n same_worker, -- 16\n pre_run_error, -- 17\n permissioned_as_email, -- 18\n visible_to_owner, -- 19\n flow_innermost_root_job, -- 20\n root_job, -- 38\n concurrent_limit, -- 21\n concurrency_time_window_s, -- 22\n timeout, -- 23\n flow_step_id, -- 24\n cache_ttl, -- 25\n priority, -- 26\n trigger_kind, -- 39\n script_entrypoint_override, -- 12\n preprocessed, -- 27,\n labels -- 44\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27,\n -- $44 (payload labels) merged with the labels of the runnable's folder, if any\n -- ($45/$46 are runnable_path/workspace_id again, kept separate to avoid parameter type conflicts)\n (SELECT CASE WHEN fl.labels IS NULL THEN $44\n ELSE (SELECT array_agg(DISTINCT lbl) FROM unnest(COALESCE($44, ARRAY[]::TEXT[]) || fl.labels) lbl)\n END\n FROM folder_labels($46, $45) AS fl(labels)))\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id, end_user_email, job_token_scopes)\n values ($1, $32, $33, $34, $35, $36, $37, $2, $41, $47)\n ON CONFLICT (job_id) DO UPDATE SET email = EXCLUDED.email, username = EXCLUDED.username, is_admin = EXCLUDED.is_admin, is_operator = EXCLUDED.is_operator, folders = EXCLUDED.folders, groups = EXCLUDED.groups, workspace_id = EXCLUDED.workspace_id, end_user_email = EXCLUDED.end_user_email, job_token_scopes = EXCLUDED.job_token_scopes\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority, cache_ignore_s3_path, runnable_settings_handle)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31, $42, $43)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Uuid",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "job_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"preview",
|
||||
"flow",
|
||||
"dependencies",
|
||||
"flowpreview",
|
||||
"script_hub",
|
||||
"identity",
|
||||
"flowdependencies",
|
||||
"http",
|
||||
"graphql",
|
||||
"postgresql",
|
||||
"noop",
|
||||
"appdependencies",
|
||||
"deploymentcallback",
|
||||
"singlestepflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript",
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Varchar",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "script_lang",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
"snowflake",
|
||||
"graphql",
|
||||
"powershell",
|
||||
"mssql",
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby",
|
||||
"rlang",
|
||||
"dbt"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Bool",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Uuid",
|
||||
"Int4",
|
||||
"Int4",
|
||||
"Int4",
|
||||
"Varchar",
|
||||
"Int4",
|
||||
"Int2",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Timestamptz",
|
||||
"Varchar",
|
||||
"Int2",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"JsonbArray",
|
||||
"TextArray",
|
||||
"Uuid",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "job_trigger_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"webhook",
|
||||
"http",
|
||||
"websocket",
|
||||
"kafka",
|
||||
"email",
|
||||
"nats",
|
||||
"schedule",
|
||||
"app",
|
||||
"ui",
|
||||
"postgres",
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud",
|
||||
"google",
|
||||
"ci_test",
|
||||
"github",
|
||||
"azure",
|
||||
"asset",
|
||||
"freshness",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Int8",
|
||||
"TextArray",
|
||||
"Text",
|
||||
"Text",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "095fa75c60724664f0355cd6e3a64f84a0f1a31cd6732c0ab994a2d6ac3c3eec"
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT DISTINCT ON (path)\n path,\n value as \"value!: sqlx::types::Json<Box<serde_json::value::RawValue>>\",\n created_at,\n email IS NULL as \"legacy!\",\n typ::text as \"typ!\"\n FROM draft\n WHERE workspace_id = $1\n AND typ IN ('app', 'raw_app')\n AND (email = $2 OR email IS NULL)\n AND NOT EXISTS (\n SELECT 1 FROM app a\n WHERE a.workspace_id = draft.workspace_id\n AND a.path = draft.path\n )\n ORDER BY path, (email IS NULL), created_at DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "value!: sqlx::types::Json<Box<serde_json::value::RawValue>>",
|
||||
"type_info": "Json"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "legacy!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "typ!",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "0d260ba4f168d725ea3b82d33324dd1d16a68a2bae6e500d9e480b152fd5859d"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS (\n SELECT 1 FROM v2_job o, v2_job t,\n LATERAL (SELECT ARRAY_REMOVE(ARRAY[o.id, o.parent_job, o.root_job,\n o.flow_innermost_root_job], NULL) AS run) l\n WHERE o.id = $1 AND t.id = $2 AND t.workspace_id = o.workspace_id\n AND (t.id = ANY(l.run) OR t.parent_job = ANY(l.run)\n OR t.root_job = ANY(l.run) OR t.flow_innermost_root_job = ANY(l.run))\n )",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "10f166464a10fc35df2b6ff1b497b17d6bb71f0ae69cd218e6a3fbc151d19d87"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT job_token_scopes FROM job_perms WHERE job_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "job_token_scopes",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "13f358b7c76cda0b24fcb17dbbe10a00333bbf3b8b43bbccc7a6e1926d82478c"
|
||||
}
|
||||
+324
@@ -0,0 +1,324 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n v2_job_queue.workspace_id,\n v2_job_queue.id,\n v2_job.args as \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",\n v2_job.parent_job,\n v2_job.created_by,\n v2_job_queue.started_at,\n v2_job_queue.runnable_settings_handle,\n scheduled_for,\n runnable_path,\n kind as \"kind: JobKind\",\n runnable_id as \"runnable_id: ScriptHash\",\n canceled_reason,\n canceled_by,\n permissioned_as,\n permissioned_as_email,\n flow_status as \"flow_status: sqlx::types::Json<Box<RawValue>>\",\n v2_job.tag,\n script_lang as \"script_lang: ScriptLang\",\n same_worker,\n pre_run_error,\n concurrent_limit,\n concurrency_time_window_s,\n flow_innermost_root_job,\n root_job,\n timeout,\n flow_step_id,\n cache_ttl,\n cache_ignore_s3_path,\n v2_job_queue.priority,\n preprocessed,\n script_entrypoint_override,\n trigger,\n trigger_kind as \"trigger_kind: TriggerKindLabel\",\n visible_to_owner,\n NULL as permissioned_as_end_user_email,\n job_perms.job_token_scopes\n FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id\n LEFT JOIN job_perms ON job_perms.job_id = v2_job_queue.id WHERE v2_job_queue.id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "args: sqlx::types::Json<HashMap<String, Box<RawValue>>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "parent_job",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "created_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "started_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "runnable_settings_handle",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "scheduled_for",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "runnable_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "kind: JobKind",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "job_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"preview",
|
||||
"flow",
|
||||
"dependencies",
|
||||
"flowpreview",
|
||||
"script_hub",
|
||||
"identity",
|
||||
"flowdependencies",
|
||||
"http",
|
||||
"graphql",
|
||||
"postgresql",
|
||||
"noop",
|
||||
"appdependencies",
|
||||
"deploymentcallback",
|
||||
"singlestepflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript",
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "runnable_id: ScriptHash",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "canceled_reason",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "canceled_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "permissioned_as",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "permissioned_as_email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "flow_status: sqlx::types::Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "tag",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "script_lang: ScriptLang",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "script_lang",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
"snowflake",
|
||||
"graphql",
|
||||
"powershell",
|
||||
"mssql",
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby",
|
||||
"rlang",
|
||||
"dbt"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "same_worker",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "pre_run_error",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "concurrent_limit",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "concurrency_time_window_s",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "flow_innermost_root_job",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "root_job",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "timeout",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"name": "flow_step_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 26,
|
||||
"name": "cache_ttl",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 27,
|
||||
"name": "cache_ignore_s3_path",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 28,
|
||||
"name": "priority",
|
||||
"type_info": "Int2"
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"name": "preprocessed",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 30,
|
||||
"name": "script_entrypoint_override",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 31,
|
||||
"name": "trigger",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 32,
|
||||
"name": "trigger_kind: TriggerKindLabel",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "job_trigger_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"webhook",
|
||||
"http",
|
||||
"websocket",
|
||||
"kafka",
|
||||
"email",
|
||||
"nats",
|
||||
"schedule",
|
||||
"app",
|
||||
"ui",
|
||||
"postgres",
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud",
|
||||
"google",
|
||||
"ci_test",
|
||||
"github",
|
||||
"azure",
|
||||
"asset",
|
||||
"freshness",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 33,
|
||||
"name": "visible_to_owner",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 34,
|
||||
"name": "permissioned_as_end_user_email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 35,
|
||||
"name": "job_token_scopes",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
null,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "333f7116d660756c36dfc0aa2b8d7ebd10a38cbe5724a554a12291c287b67f6d"
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH RECURSIVE chain(origin, id, parent_job) AS (\n SELECT id, id, parent_job FROM v2_job WHERE id = ANY($1) AND workspace_id = $2\n UNION ALL\n SELECT c.origin, j.id, j.parent_job FROM v2_job j\n JOIN chain c ON j.id = c.parent_job AND j.workspace_id = $2\n )\n SELECT c.origin AS \"origin!\",\n CASE WHEN a.agent THEN regexp_replace(j.runnable_path, '\\.chat$', '')\n ELSE j.runnable_path END AS \"runnable_path!\"\n FROM chain c JOIN v2_job j ON j.id = c.id,\n LATERAL (SELECT j.kind = 'flowpreview'\n AND j.raw_flow->'modules'->1 IS NULL\n AND j.raw_flow->'modules'->0->>'id' = '__wm_agent_root' AS agent) a\n WHERE j.workspace_id = $2 AND j.runnable_path IS NOT NULL\n AND (a.agent OR j.kind IN ('script', 'script_hub', 'unassigned_script', 'flow',\n 'unassigned_flow', 'singlestepflow', 'unassigned_singlestepflow'))",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "origin!",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "runnable_path!",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"UuidArray",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "35f960e9de880fdcaf81615933c828c4a877709c3c39a530d629cc91f9dd2d29"
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n flow_version.id AS version,\n flow_version.value->>'early_return' as early_return,\n flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor,\n flow_version.value->>'failure_module' IS NOT NULL as has_failure_module,\n (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled,\n flow.tag,\n flow.dedicated_worker,\n flow.on_behalf_of,\n flow.edited_by,\n flow.labels,\n flow.job_token_scopes\n FROM\n flow_version\n INNER JOIN flow\n ON flow.path = flow_version.path AND\n flow.workspace_id = flow_version.workspace_id\n WHERE\n flow_version.workspace_id = $1 AND\n flow_version.path = $2 AND\n flow_version.id = $3\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "version",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "early_return",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "has_preprocessor",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "has_failure_module",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "chat_input_enabled",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "tag",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "dedicated_worker",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "on_behalf_of",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "edited_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "labels",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "job_token_scopes",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "4631200a37f7edec92f3796ab4d4585d9c2aba36fdf33bca9739049366bed107"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id, job_token_scopes)\n SELECT unnest($1::uuid[]), $2, $3, $4, $5, $6, $7, $8, $9",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"UuidArray",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"JsonbArray",
|
||||
"TextArray",
|
||||
"Varchar",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4cc81f5e04caefa4f170807c0ce96c69f22d9ea9dc3586099553a8f0e76aad82"
|
||||
}
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT DISTINCT ON (path)\n path,\n value as \"value!: sqlx::types::Json<Box<serde_json::value::RawValue>>\",\n created_at\n FROM draft\n WHERE workspace_id = $1\n AND typ = 'flow'\n AND (email = $2 OR email IS NULL)\n AND NOT EXISTS (\n SELECT 1 FROM flow f\n WHERE f.workspace_id = draft.workspace_id\n AND f.path = draft.path\n )\n ORDER BY path, (email IS NULL)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "value!: sqlx::types::Json<Box<serde_json::value::RawValue>>",
|
||||
"type_info": "Json"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5223412e366b22d953867425ed6156a66727e7a469abad1664ee03a3da31078a"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO flow\n (workspace_id, path, summary, description, archived, extra_perms, dependency_job, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at, labels, job_token_scopes)\n SELECT workspace_id, $1, summary, description, archived, extra_perms, dependency_job, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at, labels, job_token_scopes\n FROM flow\n WHERE path = $2 AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "580271c5fe178f5e31f3e211cd7fea97aec4d6d3ec65d3e7402811c7dddbbf64"
|
||||
}
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n j.id, j.workspace_id, j.runnable_id AS \"runnable_id: ScriptHash\", q.scheduled_for, q.started_at, j.parent_job, j.flow_innermost_root_job, j.runnable_path, j.kind as \"kind!: JobKind\", j.permissioned_as,\n j.created_by, j.script_lang AS \"script_lang: ScriptLang\", j.permissioned_as_email, j.flow_step_id, j.trigger_kind AS \"trigger_kind: TriggerKindLabel\", j.trigger, j.priority, j.concurrent_limit, j.tag, j.cache_ttl, q.cache_ignore_s3_path, q.runnable_settings_handle,\n COALESCE(j.args->'build_binary_only' = 'true'::jsonb, false) AS \"build_binary_only!\",\n p.job_token_scopes AS \"job_token_scopes?\"\n FROM v2_job j LEFT JOIN v2_job_queue q ON j.id = q.id\n LEFT JOIN job_perms p ON p.job_id = j.id\n WHERE j.id = $1 AND j.workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "runnable_id: ScriptHash",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "scheduled_for",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "started_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "parent_job",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "flow_innermost_root_job",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "runnable_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "kind!: JobKind",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "job_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"preview",
|
||||
"flow",
|
||||
"dependencies",
|
||||
"flowpreview",
|
||||
"script_hub",
|
||||
"identity",
|
||||
"flowdependencies",
|
||||
"http",
|
||||
"graphql",
|
||||
"postgresql",
|
||||
"noop",
|
||||
"appdependencies",
|
||||
"deploymentcallback",
|
||||
"singlestepflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript",
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "permissioned_as",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "created_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "script_lang: ScriptLang",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "script_lang",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
"snowflake",
|
||||
"graphql",
|
||||
"powershell",
|
||||
"mssql",
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby",
|
||||
"rlang",
|
||||
"dbt"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "permissioned_as_email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "flow_step_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "trigger_kind: TriggerKindLabel",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "job_trigger_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"webhook",
|
||||
"http",
|
||||
"websocket",
|
||||
"kafka",
|
||||
"email",
|
||||
"nats",
|
||||
"schedule",
|
||||
"app",
|
||||
"ui",
|
||||
"postgres",
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud",
|
||||
"google",
|
||||
"ci_test",
|
||||
"github",
|
||||
"azure",
|
||||
"asset",
|
||||
"freshness",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "trigger",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "priority",
|
||||
"type_info": "Int2"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "concurrent_limit",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "tag",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "cache_ttl",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "cache_ignore_s3_path",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "runnable_settings_handle",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "build_binary_only!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "job_token_scopes?",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
null,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5a975e486a0cdb0fae4dadd344ad3abc2f67827597ef74fef473e309b1951544"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT job_token_scopes FROM script WHERE hash = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "job_token_scopes",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "60d53c73e67dd7b29cce910fd76b6c87f2994fa2d5f33e0f3ee9effe28614abe"
|
||||
}
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE v2_job_status SET\n flow_status = JSONB_SET(\n JSONB_SET(v2_job_status.flow_status, $2::TEXT[], $3),\n $4::TEXT[], $5\n ) - $6::TEXT[],\n flow_leaf_jobs = CASE\n WHEN $7::TEXT IS NULL\n OR COALESCE(v2_job.flow_innermost_root_job, v2_job_status.id) <> v2_job_status.id\n THEN v2_job_status.flow_leaf_jobs\n ELSE JSONB_SET(COALESCE(v2_job_status.flow_leaf_jobs, '{}'::JSONB), ARRAY[$7::TEXT], $8) END\n FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id\n LEFT JOIN job_perms ON job_perms.job_id = v2_job_queue.id\n WHERE v2_job_status.id = $1 AND v2_job_queue.id = $1\n RETURNING\n v2_job_queue.workspace_id,\n v2_job_queue.id,\n v2_job.args as \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",\n v2_job.parent_job,\n v2_job.created_by,\n v2_job_queue.started_at,\n v2_job_queue.runnable_settings_handle,\n v2_job_queue.scheduled_for,\n v2_job.runnable_path,\n v2_job.kind as \"kind: JobKind\",\n v2_job.runnable_id as \"runnable_id: ScriptHash\",\n v2_job_queue.canceled_reason,\n v2_job_queue.canceled_by,\n v2_job.permissioned_as,\n v2_job.permissioned_as_email,\n v2_job_status.flow_status as \"flow_status: sqlx::types::Json<Box<RawValue>>\",\n v2_job.tag,\n v2_job.script_lang as \"script_lang: ScriptLang\",\n v2_job.same_worker,\n v2_job.pre_run_error,\n v2_job.concurrent_limit,\n v2_job.concurrency_time_window_s,\n v2_job.flow_innermost_root_job,\n v2_job.root_job,\n v2_job.timeout,\n v2_job.flow_step_id,\n v2_job.cache_ttl,\n v2_job_queue.cache_ignore_s3_path,\n v2_job_queue.priority,\n v2_job.preprocessed,\n v2_job.script_entrypoint_override,\n v2_job.trigger,\n v2_job.trigger_kind as \"trigger_kind: TriggerKindLabel\",\n v2_job.visible_to_owner,\n NULL as permissioned_as_end_user_email,\n job_perms.job_token_scopes",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "args: sqlx::types::Json<HashMap<String, Box<RawValue>>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "parent_job",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "created_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "started_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "runnable_settings_handle",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "scheduled_for",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "runnable_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "kind: JobKind",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "job_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"preview",
|
||||
"flow",
|
||||
"dependencies",
|
||||
"flowpreview",
|
||||
"script_hub",
|
||||
"identity",
|
||||
"flowdependencies",
|
||||
"http",
|
||||
"graphql",
|
||||
"postgresql",
|
||||
"noop",
|
||||
"appdependencies",
|
||||
"deploymentcallback",
|
||||
"singlestepflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript",
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "runnable_id: ScriptHash",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "canceled_reason",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "canceled_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "permissioned_as",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "permissioned_as_email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "flow_status: sqlx::types::Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "tag",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "script_lang: ScriptLang",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "script_lang",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
"snowflake",
|
||||
"graphql",
|
||||
"powershell",
|
||||
"mssql",
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby",
|
||||
"rlang",
|
||||
"dbt"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "same_worker",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "pre_run_error",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "concurrent_limit",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "concurrency_time_window_s",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "flow_innermost_root_job",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "root_job",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "timeout",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"name": "flow_step_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 26,
|
||||
"name": "cache_ttl",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 27,
|
||||
"name": "cache_ignore_s3_path",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 28,
|
||||
"name": "priority",
|
||||
"type_info": "Int2"
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"name": "preprocessed",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 30,
|
||||
"name": "script_entrypoint_override",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 31,
|
||||
"name": "trigger",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 32,
|
||||
"name": "trigger_kind: TriggerKindLabel",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "job_trigger_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"webhook",
|
||||
"http",
|
||||
"websocket",
|
||||
"kafka",
|
||||
"email",
|
||||
"nats",
|
||||
"schedule",
|
||||
"app",
|
||||
"ui",
|
||||
"postgres",
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud",
|
||||
"google",
|
||||
"ci_test",
|
||||
"github",
|
||||
"azure",
|
||||
"asset",
|
||||
"freshness",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 33,
|
||||
"name": "visible_to_owner",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 34,
|
||||
"name": "permissioned_as_end_user_email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 35,
|
||||
"name": "job_token_scopes",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"TextArray",
|
||||
"Jsonb",
|
||||
"TextArray",
|
||||
"Jsonb",
|
||||
"TextArray",
|
||||
"Text",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
null,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "6445041033c95fcf52c79c33f34cf6c41fb443f8bd91f12bce0e53f6e91bfb16"
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT $2 IN (parent_job, root_job, flow_innermost_root_job) FROM v2_job\n WHERE id = $1 AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "67db536d06c57f195641f178ce651cbd7b8896984115d55318bb6f4100d72d9a"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO script (\n workspace_id, hash, path, parent_hashes, summary, description, content,\n created_by, created_at, archived, schema, deleted, is_template,\n extra_perms, lock, lock_error_logs, language, kind, tag,\n envs, concurrent_limit, concurrency_time_window_s, cache_ttl,\n dedicated_worker, ws_error_handler_muted, priority, timeout,\n delete_after_use, delete_after_secs, restart_unless_cancelled, concurrency_key,\n visible_to_runner_only, auto_kind, codebase, has_preprocessor,\n on_behalf_of, on_behalf_of_email, assets, modules, job_token_scopes\n )\n SELECT\n $1, hash, path, parent_hashes, summary, description, content,\n created_by, created_at, archived, schema, deleted, is_template,\n extra_perms, lock, lock_error_logs, language, kind, tag,\n envs, concurrent_limit, concurrency_time_window_s, cache_ttl,\n dedicated_worker, ws_error_handler_muted, priority, timeout,\n delete_after_use, delete_after_secs, restart_unless_cancelled, concurrency_key,\n visible_to_runner_only, auto_kind, codebase, has_preprocessor,\n -- Same three forms and the same prefix-first rule as permissioned_as_exists,\n -- superadmin fallback included: one acting outside their workspaces has no usr\n -- row but still authenticates.\n CASE WHEN on_behalf_of LIKE 'u/%' THEN\n (SELECT on_behalf_of WHERE EXISTS (\n SELECT 1 FROM usr u WHERE u.workspace_id = $1::varchar\n AND u.username = substring(on_behalf_of from 3)\n UNION ALL\n SELECT 1 FROM password p WHERE p.super_admin\n AND (p.username = substring(on_behalf_of from 3)\n OR p.email = substring(on_behalf_of from 3))))\n WHEN on_behalf_of LIKE 'g/%' THEN\n (SELECT on_behalf_of WHERE EXISTS (\n SELECT 1 FROM group_ g WHERE g.workspace_id = $1::varchar\n AND g.name = substring(on_behalf_of from 3)))\n ELSE\n (SELECT on_behalf_of WHERE EXISTS (\n SELECT 1 FROM usr u WHERE u.workspace_id = $1::varchar\n AND u.username = on_behalf_of\n UNION ALL\n SELECT 1 FROM password p WHERE p.email = on_behalf_of\n AND p.super_admin))\n END, on_behalf_of_email, assets, modules, job_token_scopes\n FROM script\n WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "706f2227e34971d22a577b33f6aaa7f5755da9997419c4a9d07b30d4da318bea"
|
||||
}
|
||||
+155
File diff suppressed because one or more lines are too long
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT coalesce(flow_version_lite.value, flow_version.value) as \"value!: sqlx::types::Json<Box<RawValue>>\", flow.job_token_scopes FROM flow\n LEFT JOIN flow_version\n ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]\n LEFT JOIN flow_version_lite\n ON flow_version_lite.id = flow_version.id\n WHERE flow.path = $1 AND flow.workspace_id = $2 LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "value!: sqlx::types::Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "job_token_scopes",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "77e9cb16c25defc2423621a64f020e1e07f7e50df82fb6c33f79e6faa5b77bab"
|
||||
}
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT restart_unless_cancelled, timeout FROM script WHERE hash = $1 AND workspace_id = $2",
|
||||
"query": "SELECT restart_unless_cancelled, timeout, job_token_scopes FROM script WHERE hash = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -12,6 +12,11 @@
|
||||
"ordinal": 1,
|
||||
"name": "timeout",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "job_token_scopes",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -21,9 +26,10 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "1debd472c9ffd2fc78877484f93db51f9aabed54f9894eda8ad610053ad76ce6"
|
||||
"hash": "83e9c830ee816d4e4e7193a67823cab7c9a4461d3bfbdc0fe7dbdd28ae2de924"
|
||||
}
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT DISTINCT ON (path)\n path,\n value as \"value!: sqlx::types::Json<Box<serde_json::value::RawValue>>\",\n created_at,\n typ::text as \"typ!\"\n FROM draft\n WHERE workspace_id = $1\n AND typ IN ('app', 'raw_app')\n AND (email = $2 OR email IS NULL)\n AND NOT EXISTS (\n SELECT 1 FROM app a\n WHERE a.workspace_id = draft.workspace_id\n AND a.path = draft.path\n )\n ORDER BY path, (email IS NULL), created_at DESC",
|
||||
"query": "SELECT DISTINCT ON (path)\n path,\n value as \"value!: sqlx::types::Json<Box<serde_json::value::RawValue>>\",\n created_at,\n email IS NULL as \"legacy!\"\n FROM draft\n WHERE workspace_id = $1\n AND typ = 'flow'\n AND (email = $2 OR email IS NULL)\n AND NOT EXISTS (\n SELECT 1 FROM flow f\n WHERE f.workspace_id = draft.workspace_id\n AND f.path = draft.path\n )\n ORDER BY path, (email IS NULL)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -20,8 +20,8 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "typ!",
|
||||
"type_info": "Text"
|
||||
"name": "legacy!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -37,5 +37,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "1860c102a5309a8e1bb7288d0616f48e4330af7fbe61426e999c6073f630d88f"
|
||||
"hash": "8f5091626054a43be07768f30cdc99bd68f1de8064ace4155f4da2cd45d1b403"
|
||||
}
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT DISTINCT ON (path)\n path,\n value as \"value!: sqlx::types::Json<Box<serde_json::value::RawValue>>\",\n created_at\n FROM draft\n WHERE workspace_id = $1\n AND typ = 'script'\n AND (email = $2 OR email IS NULL)\n AND NOT EXISTS (\n SELECT 1 FROM script s\n WHERE s.workspace_id = draft.workspace_id\n AND s.path = draft.path\n )\n ORDER BY path, (email IS NULL)",
|
||||
"query": "SELECT DISTINCT ON (path)\n path,\n value as \"value!: sqlx::types::Json<Box<serde_json::value::RawValue>>\",\n created_at,\n email IS NULL as \"legacy!\"\n FROM draft\n WHERE workspace_id = $1\n AND typ = 'script'\n AND (email = $2 OR email IS NULL)\n AND NOT EXISTS (\n SELECT 1 FROM script s\n WHERE s.workspace_id = draft.workspace_id\n AND s.path = draft.path\n )\n ORDER BY path, (email IS NULL)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -17,6 +17,11 @@
|
||||
"ordinal": 2,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "legacy!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -28,8 +33,9 @@
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "fac99e27c8396185dd21c33baf649dd75b5f8ed859e410d2322ec3f565382fa8"
|
||||
"hash": "9a29bd07a66f31f7414b79cafd8073194c6990eedad4b5b540134dd13d72abb1"
|
||||
}
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO flow_conversation_message (conversation_id, message_type, content, job_id, step_name, success, tool_arguments, tool_result, reasoning, attachments)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
|
||||
"query": "INSERT INTO flow_conversation_message (conversation_id, message_type, content, job_id, turn_job_id, step_name, success, tool_arguments, tool_result, reasoning, attachments)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -20,6 +20,7 @@
|
||||
},
|
||||
"Text",
|
||||
"Uuid",
|
||||
"Uuid",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Text",
|
||||
@@ -30,5 +31,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "12329c3359a7944ab5fa3aa27ddca1b26f340ccf574b9fa07641fe88b2d2987c"
|
||||
"hash": "a6dafb34be6088886b078ed2b554de8c72e312ef8abd5ed29482a59c8ec51dd7"
|
||||
}
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT q.id AS \"id!\", j.created_by, j.permissioned_as, j.permissioned_as_email, j.trigger, j.trigger_kind AS \"trigger_kind: TriggerKindLabel\", j.preprocessed, j.args AS \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\" FROM v2_job_queue q JOIN v2_job j USING (id) JOIN script s ON s.workspace_id = j.workspace_id AND s.hash = j.runnable_id WHERE j.workspace_id = $1 AND j.runnable_path = $2 AND j.kind = 'script' AND j.flow_step_id IS NULL AND j.runnable_id != $3 AND q.canceled_by IS NULL AND s.restart_unless_cancelled",
|
||||
"query": "SELECT q.id AS \"id!\", j.created_by, j.permissioned_as, j.permissioned_as_email, j.trigger, j.trigger_kind AS \"trigger_kind: TriggerKindLabel\", j.preprocessed, j.args AS \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\", p.job_token_scopes AS \"job_token_scopes?\" FROM v2_job_queue q JOIN v2_job j USING (id) JOIN script s ON s.workspace_id = j.workspace_id AND s.hash = j.runnable_id LEFT JOIN job_perms p ON p.job_id = q.id WHERE j.workspace_id = $1 AND j.runnable_path = $2 AND j.kind = 'script' AND j.flow_step_id IS NULL AND j.runnable_id != $3 AND q.canceled_by IS NULL AND s.restart_unless_cancelled",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -71,6 +71,11 @@
|
||||
"ordinal": 7,
|
||||
"name": "args: sqlx::types::Json<HashMap<String, Box<RawValue>>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "job_token_scopes?",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -88,8 +93,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "df8586283178b0e684b9a0e359efb16e38fd68a5fe2b01cd77f74a725bbf37a3"
|
||||
"hash": "aed606f8c8d91a924ac457b4eabdf391947d5fc3da90ef98a7b793c2430eb7bc"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO flow\n (workspace_id, path, summary, description, archived, extra_perms, dependency_job, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at, job_token_scopes)\n SELECT workspace_id, REGEXP_REPLACE(path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1'), summary, description, archived, extra_perms, dependency_job, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at, job_token_scopes\n FROM flow\n WHERE path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b40c9e2d637cf0f2f49fd4c720b64eb6af24e8d8aa99b846f21f2ed49c320bdc"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT job_token_scopes FROM script WHERE path = $1 AND workspace_id = $2 AND deleted = false ORDER BY created_at DESC LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "job_token_scopes",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b48afe9913423a90955011fc6368b8517561f5b55a30e44801d6a9123a762584"
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, delete_after_secs, timeout, concurrency_key, visible_to_runner_only, auto_kind, codebase, has_preprocessor, schema_validation, assets, debounce_key, debounce_delay_s, cache_ignore_s3_path, runnable_settings_handle, modules, labels, on_behalf_of, on_behalf_of_email, job_token_scopes) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Int8Array",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "script_lang",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
"snowflake",
|
||||
"graphql",
|
||||
"powershell",
|
||||
"mssql",
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby",
|
||||
"rlang",
|
||||
"dbt"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Custom": {
|
||||
"name": "script_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"trigger",
|
||||
"failure",
|
||||
"command",
|
||||
"approval",
|
||||
"preprocessor"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Varchar",
|
||||
"VarcharArray",
|
||||
"Int4",
|
||||
"Int4",
|
||||
"Int4",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Int2",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Int4",
|
||||
"Int4",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Int4",
|
||||
"Bool",
|
||||
"Int8",
|
||||
"Jsonb",
|
||||
"TextArray",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "bfd01122256e922b00acf0e86593007c6aeccf76bfd07384ffaccf50a24d13d4"
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH inserted AS (\n INSERT INTO flow\n (workspace_id, path, summary, description, archived, extra_perms, dependency_job, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at, labels, lock_error_logs, job_token_scopes)\n SELECT workspace_id, REGEXP_REPLACE(path, 'u/' || $2 || '/(.*)', $1 || '/\\1'), summary, description, archived, extra_perms, dependency_job, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at, labels, lock_error_logs, job_token_scopes\n FROM flow\n WHERE path LIKE ('u/' || $2 || '/%') AND workspace_id = $3\n RETURNING 1\n ) SELECT COUNT(*) FROM inserted",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "c4eaa6e3b1d30a72ef1b54b2b7f28d84f1ca3c91de86d520dce06ac12d557bd5"
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT email, username, is_admin, is_operator, groups, folders, end_user_email,\n job_token_scopes\n FROM job_perms WHERE job_id = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "is_admin",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "is_operator",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "groups",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "folders",
|
||||
"type_info": "JsonbArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "end_user_email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "job_token_scopes",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "c921b1a03e3181bbac351545941affc87853ff2d97f567a8817260cfef43e00e"
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE\n flow\n SET\n path = $1,\n summary = $2,\n description = $3,\n dependency_job = NULL,\n lock_error_logs = '',\n tag = $4,\n dedicated_worker = $5,\n visible_to_runner_only = $6,\n ws_error_handler_muted = $7,\n value = $8,\n schema = $9::text::json,\n edited_by = $10,\n edited_at = now(),\n labels = COALESCE($13, labels),\n on_behalf_of = $14,\n on_behalf_of_email = $15,\n job_token_scopes = CASE WHEN $16 THEN $17 ELSE job_token_scopes END\n WHERE\n path = $11 AND workspace_id = $12",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"TextArray",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Bool",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ce3bccee14cd107631c4ddb4a467099be925f1058c76c6af31e8a620607cf6a1"
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "select hash, tag, concurrency_key, concurrent_limit, concurrency_time_window_s, debounce_key, debounce_delay_s, cache_ttl, cache_ignore_s3_path, runnable_settings_handle, language as \"language: ScriptLang\", dedicated_worker, priority, timeout, on_behalf_of, created_by, labels, job_token_scopes FROM script\n WHERE path = $1 AND workspace_id = $2 AND archived = false AND (lock IS NOT NULL OR $3 = false)\n ORDER BY created_at DESC LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "hash",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "tag",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "concurrency_key",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "concurrent_limit",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "concurrency_time_window_s",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "debounce_key",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "debounce_delay_s",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "cache_ttl",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "cache_ignore_s3_path",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "runnable_settings_handle",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "language: ScriptLang",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "script_lang",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
"snowflake",
|
||||
"graphql",
|
||||
"powershell",
|
||||
"mssql",
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby",
|
||||
"rlang",
|
||||
"dbt"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "dedicated_worker",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "priority",
|
||||
"type_info": "Int2"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "timeout",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "on_behalf_of",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "created_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "labels",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "job_token_scopes",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d51f555e80b27549b884f334087878286dcc2f23101c2e1c8c1d07b6f0deaecd"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM flow_conversation_message WHERE job_id = ANY($1) RETURNING conversation_id",
|
||||
"query": "DELETE FROM flow_conversation_message WHERE COALESCE(turn_job_id, job_id) = ANY($1)\n RETURNING conversation_id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -18,5 +18,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "967f52005f4a044b3a2e9f02ceadf90dad5246681bde6caaa633b85a5e8b2352"
|
||||
"hash": "d5f1e272af4a428daedafa3453124b1973a48d47c1c563b2213055bfcf688230"
|
||||
}
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n COUNT(*) FILTER (WHERE NOT operator AND NOT disabled AND NOT is_service_account) AS \"developers!\",\n COUNT(*) FILTER (WHERE operator AND NOT disabled AND NOT is_service_account) AS \"operators!\"\n FROM usr WHERE workspace_id = $1",
|
||||
"query": "SELECT\n COUNT(*) FILTER (WHERE NOT operator AND NOT disabled AND NOT is_service_account) AS \"developers!\",\n COUNT(*) FILTER (WHERE operator AND NOT disabled AND NOT is_service_account) AS \"operators!\",\n COUNT(*) FILTER (WHERE is_service_account AND NOT disabled) AS \"service_accounts!\"\n FROM usr WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -12,6 +12,11 @@
|
||||
"ordinal": 1,
|
||||
"name": "operators!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "service_accounts!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -20,9 +25,10 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "ebce1633afe3d3f8a1a68bc8d26adcdd88b137bb892437a3ca67fd716772512b"
|
||||
"hash": "d61e8523269a26051079b0ee837bf33c1ce2e063aaf80fd155dbbbf99eb2fab3"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO flow (\n workspace_id, path, summary, description, value, edited_by, edited_at,\n archived, schema, extra_perms, dependency_job, tag,\n ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only,\n concurrency_key, versions, on_behalf_of, on_behalf_of_email, lock_error_logs,\n job_token_scopes\n )\n SELECT $2, path, summary, description, value, edited_by, edited_at,\n archived, schema, extra_perms, NULL, tag,\n ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only,\n concurrency_key, ARRAY[]::bigint[],\n -- Same predicate as clone_scripts.\n CASE WHEN on_behalf_of LIKE 'u/%' THEN\n (SELECT on_behalf_of WHERE EXISTS (\n SELECT 1 FROM usr u WHERE u.workspace_id = $2::varchar\n AND u.username = substring(on_behalf_of from 3)\n UNION ALL\n SELECT 1 FROM password p WHERE p.super_admin\n AND (p.username = substring(on_behalf_of from 3)\n OR p.email = substring(on_behalf_of from 3))))\n WHEN on_behalf_of LIKE 'g/%' THEN\n (SELECT on_behalf_of WHERE EXISTS (\n SELECT 1 FROM group_ g WHERE g.workspace_id = $2::varchar\n AND g.name = substring(on_behalf_of from 3)))\n ELSE\n (SELECT on_behalf_of WHERE EXISTS (\n SELECT 1 FROM usr u WHERE u.workspace_id = $2::varchar\n AND u.username = on_behalf_of\n UNION ALL\n SELECT 1 FROM password p WHERE p.email = on_behalf_of\n AND p.super_admin))\n END, on_behalf_of_email, lock_error_logs, job_token_scopes\n FROM flow\n WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "df1fe2e7ef004b8de0e626f40fd737b3b4b662a89148cf6ac70eb3563ad7f0c5"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO flow\n (workspace_id, path, summary, description, archived, extra_perms, dependency_job, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at, lock_error_logs, job_token_scopes)\n SELECT $1, path, summary, description, archived, extra_perms, dependency_job, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at, lock_error_logs, job_token_scopes\n FROM flow WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e81eaa501b4af1269a4929ec660d10fc10d9ec4dc2359f43d79fe255d101cad8"
|
||||
}
|
||||
-125
@@ -1,125 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH RECURSIVE lineage AS (\n SELECT id, parent_job, kind, runnable_path, runnable_id, trigger_kind, trigger,\n permissioned_as, args, 0 AS depth\n FROM v2_job WHERE id = $1 AND workspace_id = $2\n UNION ALL\n SELECT p.id, p.parent_job, p.kind, p.runnable_path, p.runnable_id, p.trigger_kind,\n p.trigger, p.permissioned_as, p.args, l.depth + 1\n FROM v2_job p JOIN lineage l ON p.id = l.parent_job\n WHERE p.workspace_id = $2 AND l.depth < 100\n )\n SELECT id AS \"id!\", parent_job, kind AS \"kind!: JobKind\", runnable_path, runnable_id,\n trigger_kind::text AS trigger_kind, permissioned_as AS \"permissioned_as!\",\n -- A restart takes its flow version from the request, so a trusted flow path\n -- can carry another flow's code: the version must belong to that path.\n CASE kind\n WHEN 'flow' THEN EXISTS (SELECT 1 FROM flow_version fv\n WHERE fv.id = runnable_id AND fv.path = runnable_path AND fv.workspace_id = $2)\n WHEN 'script' THEN EXISTS (SELECT 1 FROM script s\n WHERE s.hash = runnable_id AND s.path = runnable_path AND s.workspace_id = $2\n AND NOT s.deleted)\n ELSE true\n END AS \"origin_verified!\",\n -- What `run/f` and `run/p` resolve the path to now; for scripts the predicate of\n -- `get_latest_deployed_script_hash`.\n COALESCE(CASE kind\n WHEN 'flow' THEN runnable_id = (SELECT f.versions[array_upper(f.versions, 1)]\n FROM flow f WHERE f.path = runnable_path AND f.workspace_id = $2)\n WHEN 'script' THEN runnable_id = (SELECT s.hash FROM script s\n WHERE s.path = runnable_path AND s.workspace_id = $2 AND NOT s.deleted\n AND s.lock IS NOT NULL AND s.lock_error_logs IS NULL\n ORDER BY s.created_at DESC LIMIT 1)\n END, false) AS \"current_version!\",\n EXISTS (SELECT 1 FROM v2_job_status st WHERE st.id = lineage.id\n AND jsonb_typeof(st.flow_status->'restarted_from') = 'object')\n OR EXISTS (SELECT 1 FROM v2_job_completed c WHERE c.id = lineage.id\n AND jsonb_typeof(c.flow_status->'restarted_from') = 'object')\n AS \"restarted!\",\n COALESCE(\n (SELECT st.flow_status->'restarted_from'->>'flow_job_id' FROM v2_job_status st\n WHERE st.id = lineage.id),\n (SELECT c.flow_status->'restarted_from'->>'flow_job_id' FROM v2_job_completed c\n WHERE c.id = lineage.id)\n ) AS restarted_from,\n -- Only deployed-app runs are stamped with their app; an app editor preview\n -- runs app code at an app path it does not have to own.\n COALESCE(trigger_kind = 'app' AND starts_with(runnable_path, trigger || '/'), false)\n AS \"app_stamped!\",\n -- A preview's modules come from its args, which its parent may have taken from\n -- the caller.\n COALESCE(jsonb_typeof(args->'_MODULES') = 'object', false) AS \"args_modules!\"\n FROM lineage ORDER BY depth",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id!",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "parent_job",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "kind!: JobKind",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "job_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"preview",
|
||||
"flow",
|
||||
"dependencies",
|
||||
"flowpreview",
|
||||
"script_hub",
|
||||
"identity",
|
||||
"flowdependencies",
|
||||
"http",
|
||||
"graphql",
|
||||
"postgresql",
|
||||
"noop",
|
||||
"appdependencies",
|
||||
"deploymentcallback",
|
||||
"singlestepflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript",
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "runnable_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "runnable_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "trigger_kind",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "permissioned_as!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "origin_verified!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "current_version!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "restarted!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "restarted_from",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "app_stamped!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "args_modules!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "ee92100fcd681515ba81f23970e64af7f33464879bef4d1eff682126dffeef64"
|
||||
}
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n v2_job.id,\n v2_job.raw_code,\n v2_job.raw_lock,\n v2_job.raw_flow as \"raw_flow: _\",\n v2_job.tag,\n v2_job.created_at,\n v2_job.created_by,\n v2_job.permissioned_as,\n v2_job.permissioned_as_email,\n v2_job.kind as \"kind: _\",\n v2_job.runnable_id,\n v2_job.runnable_path,\n v2_job.parent_job,\n v2_job.root_job,\n v2_job.script_lang as \"script_lang: _\",\n v2_job.script_entrypoint_override,\n v2_job.flow_step,\n v2_job.flow_step_id,\n v2_job.flow_innermost_root_job,\n v2_job.\"trigger\",\n v2_job.trigger_kind as \"trigger_kind: _\",\n v2_job.same_worker,\n v2_job.visible_to_owner,\n v2_job.concurrent_limit,\n v2_job.concurrency_time_window_s,\n v2_job.cache_ttl,\n v2_job.timeout,\n v2_job.priority,\n v2_job.preprocessed,\n v2_job.args as \"args: _\",\n v2_job.labels,\n job_perms.job_token_scopes as \"job_token_scopes?\",\n v2_job.pre_run_error,\n\n v2_job_queue.started_at,\n v2_job_queue.scheduled_for,\n v2_job_queue.running,\n v2_job_queue.canceled_by,\n v2_job_queue.canceled_reason,\n v2_job_queue.suspend,\n v2_job_queue.suspend_until,\n v2_job_queue.worker,\n v2_job_queue.extras as \"extras: _\",\n\n v2_job_runtime.ping,\n v2_job_runtime.memory_peak,\n\n v2_job_status.flow_status as \"flow_status: _\",\n v2_job_status.flow_leaf_jobs as \"flow_leaf_jobs: _\",\n v2_job_status.workflow_as_code_status as \"workflow_as_code_status: _\",\n\n concurrency_key.key as \"concurrency_key?\"\n FROM v2_job_queue\n INNER JOIN v2_job ON v2_job.id = v2_job_queue.id\n LEFT JOIN job_perms ON job_perms.job_id = v2_job_queue.id\n LEFT JOIN v2_job_runtime ON v2_job_runtime.id = v2_job_queue.id\n LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id\n LEFT JOIN concurrency_key ON concurrency_key.job_id = v2_job_queue.id\n WHERE v2_job_queue.workspace_id = $1\n AND v2_job_queue.running = false\n AND v2_job.parent_job IS NULL\n AND v2_job.trigger_kind IS DISTINCT FROM 'schedule'\n ORDER BY v2_job.created_at DESC\n LIMIT $2\n OFFSET $3\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "raw_code",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "raw_lock",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "raw_flow: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "tag",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "created_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "permissioned_as",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "permissioned_as_email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "kind: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "job_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"preview",
|
||||
"flow",
|
||||
"dependencies",
|
||||
"flowpreview",
|
||||
"script_hub",
|
||||
"identity",
|
||||
"flowdependencies",
|
||||
"http",
|
||||
"graphql",
|
||||
"postgresql",
|
||||
"noop",
|
||||
"appdependencies",
|
||||
"deploymentcallback",
|
||||
"singlestepflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript",
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "runnable_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "runnable_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "parent_job",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "root_job",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "script_lang: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "script_lang",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
"snowflake",
|
||||
"graphql",
|
||||
"powershell",
|
||||
"mssql",
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby",
|
||||
"rlang",
|
||||
"dbt"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "script_entrypoint_override",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "flow_step",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "flow_step_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "flow_innermost_root_job",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "trigger",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "trigger_kind: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "job_trigger_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"webhook",
|
||||
"http",
|
||||
"websocket",
|
||||
"kafka",
|
||||
"email",
|
||||
"nats",
|
||||
"schedule",
|
||||
"app",
|
||||
"ui",
|
||||
"postgres",
|
||||
"sqs",
|
||||
"gcp",
|
||||
"mqtt",
|
||||
"nextcloud",
|
||||
"google",
|
||||
"ci_test",
|
||||
"github",
|
||||
"azure",
|
||||
"asset",
|
||||
"freshness",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "same_worker",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "visible_to_owner",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "concurrent_limit",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "concurrency_time_window_s",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"name": "cache_ttl",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 26,
|
||||
"name": "timeout",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 27,
|
||||
"name": "priority",
|
||||
"type_info": "Int2"
|
||||
},
|
||||
{
|
||||
"ordinal": 28,
|
||||
"name": "preprocessed",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"name": "args: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 30,
|
||||
"name": "labels",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 31,
|
||||
"name": "job_token_scopes?",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 32,
|
||||
"name": "pre_run_error",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 33,
|
||||
"name": "started_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 34,
|
||||
"name": "scheduled_for",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 35,
|
||||
"name": "running",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 36,
|
||||
"name": "canceled_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 37,
|
||||
"name": "canceled_reason",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 38,
|
||||
"name": "suspend",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 39,
|
||||
"name": "suspend_until",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 40,
|
||||
"name": "worker",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 41,
|
||||
"name": "extras: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 42,
|
||||
"name": "ping",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 43,
|
||||
"name": "memory_peak",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 44,
|
||||
"name": "flow_status: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 45,
|
||||
"name": "flow_leaf_jobs: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 46,
|
||||
"name": "workflow_as_code_status: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 47,
|
||||
"name": "concurrency_key?",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "eefb19c05e6d9650b4dd1e50e10c26f01e117b75ab48e197c7d91d388865da74"
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO flow (\n workspace_id, path, summary, description,\n dependency_job, lock_error_logs, tag,\n dedicated_worker, visible_to_runner_only,\n ws_error_handler_muted,\n value, schema, edited_by, edited_at, labels,\n on_behalf_of, on_behalf_of_email, job_token_scopes\n ) VALUES (\n $1, $2, $3, $4,\n NULL, '', $5,\n $6, $7,\n $8,\n $9, $10::text::json, $11, now(), $12,\n $13, $14, $15\n )",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"TextArray",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f2146082f64fcdb4acdfe92e42f084f90b3ac06139ffca98670215956150563a"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM flow_conversation_message m\n USING flow_conversation c\n WHERE m.conversation_id = c.id AND c.workspace_id = $1 AND m.job_id = ANY($2)\n RETURNING m.conversation_id",
|
||||
"query": "DELETE FROM flow_conversation_message m\n USING flow_conversation c\n WHERE m.conversation_id = c.id AND c.workspace_id = $1\n AND COALESCE(m.turn_job_id, m.job_id) = ANY($2)\n RETURNING m.conversation_id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -19,5 +19,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "69bfbe9b39414b724488532cc3b3659915d9fcb3d58f16532aaffe06c44ec976"
|
||||
"hash": "fa81e10f725586e12c77d4f6387cc76d5569621612ade0270261c172102015b4"
|
||||
}
|
||||
Generated
+193
-154
File diff suppressed because it is too large
Load Diff
+21
-34
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -88,7 +88,7 @@ members = [
|
||||
exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -249,36 +249,22 @@ sqlx-sqlite = { git = "https://github.com/windmill-labs/sqlx", rev = "6bdaee94fa
|
||||
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).
|
||||
# Pin tokio-postgres / postgres-types / postgres-protocol to
|
||||
# windmill-labs/rust-postgres, branch `windmill-describe-first`: the
|
||||
# MaterializeInc fork (postgres-replication, which upstream has not merged)
|
||||
# plus `Client::describe_typed`, which pg_executor needs before
|
||||
# query_typed_raw so result columns of types the client has not seen yet
|
||||
# (custom enums / domains, citext, postgis) don't deadlock a large result,
|
||||
# and `Client::transaction_status`, which keeps a connection left inside a
|
||||
# transaction out of the executor's cache. Each pinned commit carries a `windmill-*` tag so it stays reachable when the
|
||||
# branch is rebased onto MaterializeInc master.
|
||||
#
|
||||
# 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" }
|
||||
# The [patch.crates-io] entries force windmill-worker's pg_executor (which
|
||||
# imports `tokio_postgres::` from crates.io) onto the same fork as
|
||||
# windmill-trigger-postgres.
|
||||
tokio-postgres = { git = "https://github.com/windmill-labs/rust-postgres", rev = "b3a6443aa1ae90a4f6c4b742103ba80584598f2e" }
|
||||
postgres-types = { git = "https://github.com/windmill-labs/rust-postgres", rev = "b3a6443aa1ae90a4f6c4b742103ba80584598f2e" }
|
||||
postgres-protocol = { git = "https://github.com/windmill-labs/rust-postgres", rev = "b3a6443aa1ae90a4f6c4b742103ba80584598f2e" }
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
@@ -473,7 +459,7 @@ tower-cookies = "^0.11"
|
||||
serde = "^1"
|
||||
# 1.0.151 introduced RawValue::from_string_unchecked, which the SQL executors use
|
||||
# to avoid re-parsing every collected row.
|
||||
serde_json = { version = "^1.0.151", features = ["preserve_order", "raw_value"] }
|
||||
serde_json = { version = "^1.0.151", features = ["preserve_order", "raw_value", "float_roundtrip"] }
|
||||
serde_yml = "0.0.12"
|
||||
uuid = { version = "^1", features = ["serde", "v4", "js"] }
|
||||
thiserror = "^2"
|
||||
@@ -586,6 +572,7 @@ base64 = "^0.22.1"
|
||||
base32 = "^0"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.6"
|
||||
ryu-js = "1.0.3"
|
||||
md-5 = "0.10.6"
|
||||
sha1 = "0.10.6"
|
||||
sqlx = { version = "0.8.0", features = [
|
||||
@@ -630,8 +617,8 @@ 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"]}
|
||||
postgres-protocol = "0.6"
|
||||
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" }
|
||||
rust-postgres = { package = "tokio-postgres", git = "https://github.com/windmill-labs/rust-postgres", rev = "b3a6443aa1ae90a4f6c4b742103ba80584598f2e"}
|
||||
rust-postgres-native-tls = { package = "postgres-native-tls", git = "https://github.com/windmill-labs/rust-postgres", features = ["runtime"], rev = "b3a6443aa1ae90a4f6c4b742103ba80584598f2e" }
|
||||
bit-vec = "=0.6.3"
|
||||
mappable-rc = "^0"
|
||||
mysql_async = { version = "*", default-features = false, features = ["minimal", "default", "native-tls-tls", "rust_decimal"]}
|
||||
|
||||
@@ -1 +1 @@
|
||||
ff4d04f17721d84fb9a1f655af50ff7e4e7dcbc7
|
||||
065c2c64c3904d5c0b2766b424210d34f9e54d2d
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE flow_conversation_message DROP COLUMN turn_job_id;
|
||||
@@ -0,0 +1,23 @@
|
||||
-- The flow run a row's turn belongs to: the job the user message started. Retention collects
|
||||
-- rows by it, so a turn goes as one unit. `job_id` names the step or tool job that wrote the
|
||||
-- row, which completes, and so expires, before the run does.
|
||||
ALTER TABLE flow_conversation_message ADD COLUMN turn_job_id UUID;
|
||||
|
||||
-- Turns can overlap in one conversation, so a row's turn is the user row its job descends
|
||||
-- from. Transcript order only decides for rows whose job is already gone or that have none.
|
||||
UPDATE flow_conversation_message m
|
||||
SET turn_job_id = COALESCE(
|
||||
(SELECT u.job_id FROM v2_job j
|
||||
JOIN flow_conversation_message u
|
||||
ON u.conversation_id = m.conversation_id
|
||||
AND u.message_type = 'user'
|
||||
AND u.job_id IN (j.id, j.parent_job, j.root_job, j.flow_innermost_root_job)
|
||||
WHERE j.id = m.job_id
|
||||
LIMIT 1),
|
||||
(SELECT u.job_id FROM flow_conversation_message u
|
||||
WHERE u.conversation_id = m.conversation_id
|
||||
AND u.message_type = 'user'
|
||||
AND u.created_seq <= m.created_seq
|
||||
ORDER BY u.created_seq DESC
|
||||
LIMIT 1)
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE job_perms DROP COLUMN IF EXISTS job_token_scopes;
|
||||
ALTER TABLE flow DROP COLUMN IF EXISTS job_token_scopes;
|
||||
ALTER TABLE script DROP COLUMN IF EXISTS job_token_scopes;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE script ADD COLUMN IF NOT EXISTS job_token_scopes text[];
|
||||
ALTER TABLE flow ADD COLUMN IF NOT EXISTS job_token_scopes text[];
|
||||
ALTER TABLE job_perms ADD COLUMN IF NOT EXISTS job_token_scopes text[];
|
||||
@@ -2,7 +2,8 @@
|
||||
"github": {
|
||||
"auth_url": "https://github.com/login/oauth/authorize",
|
||||
"token_url": "https://github.com/login/oauth/access_token",
|
||||
"scopes": ["workflow", "repo"]
|
||||
"scopes": ["workflow", "repo"],
|
||||
"scope_options": ["repo", "workflow", "gist", "read:org"]
|
||||
},
|
||||
"gitlab": {
|
||||
"auth_url": "https://gitlab.com/oauth/authorize",
|
||||
@@ -43,6 +44,52 @@
|
||||
"mpim:read",
|
||||
"files:read"
|
||||
],
|
||||
"scope_options": [
|
||||
{ "value": "chat:write", "label": "Send messages as the app" },
|
||||
{ "value": "chat:write.public", "label": "Send messages to public channels the app is not in" },
|
||||
{ "value": "channels:join", "label": "Join public channels" },
|
||||
{ "value": "files:write", "label": "Upload, edit and delete files" },
|
||||
{ "value": "users:read", "label": "View people in the workspace" },
|
||||
{ "value": "users:read.email", "label": "View people's email addresses" },
|
||||
{ "value": "channels:read", "label": "View basic information about public channels" },
|
||||
{ "value": "channels:history", "label": "Read messages in public channels the app is in" },
|
||||
{ "value": "reactions:write", "label": "Add emoji reactions" },
|
||||
{ "value": "channels:manage", "label": "Create, archive and manage public channels, invite members, set topics" },
|
||||
{ "value": "groups:write", "label": "Create and manage private channels the app is in" },
|
||||
{ "value": "usergroups:write", "label": "Edit user groups (workspace settings often block bots)" },
|
||||
{ "value": "files:read", "label": "Read and download files shared where the app is a member" },
|
||||
{ "value": "groups:read", "label": "List private channels the app is in" },
|
||||
{ "value": "im:read", "label": "List the app's direct messages" },
|
||||
{ "value": "mpim:read", "label": "List the app's group direct messages" },
|
||||
{ "value": "usergroups:read", "label": "List user groups" },
|
||||
{ "value": "emoji:read", "label": "List custom emojis" }
|
||||
],
|
||||
"user_scope_options": [
|
||||
{ "value": "channels:history", "label": "Read messages in public channels" },
|
||||
{ "value": "groups:history", "label": "Read messages in your private channels" },
|
||||
{ "value": "im:history", "label": "Read your direct messages" },
|
||||
{ "value": "mpim:history", "label": "Read your group direct messages" },
|
||||
{ "value": "search:read", "label": "Search messages and files" },
|
||||
{ "value": "users:read", "label": "View people in the workspace" },
|
||||
{ "value": "chat:write", "label": "Send messages as you" },
|
||||
{ "value": "users:read.email", "label": "View people's email addresses" },
|
||||
{ "value": "channels:read", "label": "View basic information about public channels" },
|
||||
{ "value": "groups:read", "label": "View basic information about your private channels" },
|
||||
{ "value": "im:read", "label": "View basic information about your direct messages" },
|
||||
{ "value": "mpim:read", "label": "View basic information about your group direct messages" },
|
||||
{ "value": "files:read", "label": "View files shared in your conversations" },
|
||||
{ "value": "reactions:write", "label": "Add emoji reactions" },
|
||||
{ "value": "channels:write", "label": "Create, archive and manage public channels" },
|
||||
{ "value": "channels:write.invites", "label": "Invite members to public channels" },
|
||||
{ "value": "channels:write.topic", "label": "Set public channel topics" },
|
||||
{ "value": "groups:write", "label": "Create and manage private channels" },
|
||||
{ "value": "users.profile:write", "label": "Set your status and edit your profile" },
|
||||
{ "value": "reminders:write", "label": "Create reminders for yourself" },
|
||||
{ "value": "usergroups:write", "label": "Edit user groups" },
|
||||
{ "value": "files:write", "label": "Upload and delete files as yourself" },
|
||||
{ "value": "usergroups:read", "label": "List user groups" },
|
||||
{ "value": "emoji:read", "label": "List custom emojis" }
|
||||
],
|
||||
"token_response_path": "authed_user"
|
||||
},
|
||||
"supabase_wizard": {
|
||||
@@ -56,7 +103,8 @@
|
||||
"scopes": ["https://www.googleapis.com/auth/spreadsheets"],
|
||||
"scope_options": [
|
||||
{ "value": "https://www.googleapis.com/auth/spreadsheets", "label": "Read, edit, create and delete spreadsheets" },
|
||||
{ "value": "https://www.googleapis.com/auth/spreadsheets.readonly", "label": "Read spreadsheets" }
|
||||
{ "value": "https://www.googleapis.com/auth/spreadsheets.readonly", "label": "Read spreadsheets" },
|
||||
{ "value": "https://www.googleapis.com/auth/drive.metadata.readonly", "label": "List and search your spreadsheets in Drive" }
|
||||
],
|
||||
"extra_params": {
|
||||
"access_type": "offline",
|
||||
@@ -100,6 +148,9 @@
|
||||
"scope_options": [
|
||||
{ "value": "https://www.googleapis.com/auth/calendar.events", "label": "Read and edit events on all calendars" },
|
||||
{ "value": "https://www.googleapis.com/auth/calendar.events.readonly", "label": "Read events on all calendars" },
|
||||
{ "value": "https://www.googleapis.com/auth/calendar.calendarlist.readonly", "label": "See the list of calendars you're subscribed to" },
|
||||
{ "value": "https://www.googleapis.com/auth/calendar.calendars.readonly", "label": "Read calendar properties (title, timezone)" },
|
||||
{ "value": "https://www.googleapis.com/auth/calendar.freebusy", "label": "See your availability (free/busy)" },
|
||||
{ "value": "https://www.googleapis.com/auth/calendar.readonly", "label": "Read all calendars and events" },
|
||||
{ "value": "https://www.googleapis.com/auth/calendar", "label": "Read, edit, share and delete all calendars" }
|
||||
],
|
||||
@@ -130,7 +181,9 @@
|
||||
"scope_options": [
|
||||
{ "value": "https://www.googleapis.com/auth/documents", "label": "Read, edit, create and delete documents" },
|
||||
{ "value": "https://www.googleapis.com/auth/documents.readonly", "label": "Read documents" },
|
||||
{ "value": "https://www.googleapis.com/auth/drive.file", "label": "Only files created or opened with this connection" }
|
||||
{ "value": "https://www.googleapis.com/auth/drive.file", "label": "Only files created or opened with this connection" },
|
||||
{ "value": "https://www.googleapis.com/auth/drive.readonly", "label": "Read, export and search all Drive files and comments" },
|
||||
{ "value": "https://www.googleapis.com/auth/drive", "label": "Read, edit, create and delete all Drive files" }
|
||||
],
|
||||
"extra_params": {
|
||||
"access_type": "offline",
|
||||
@@ -185,7 +238,8 @@
|
||||
{ "value": "https://www.googleapis.com/auth/admin.directory.group.readonly", "label": "Read groups" },
|
||||
{ "value": "https://www.googleapis.com/auth/admin.directory.orgunit", "label": "Read and manage organizational units" },
|
||||
{ "value": "https://www.googleapis.com/auth/admin.directory.orgunit.readonly", "label": "Read organizational units" },
|
||||
{ "value": "https://www.googleapis.com/auth/admin.directory.user.security", "label": "Manage user security settings" }
|
||||
{ "value": "https://www.googleapis.com/auth/admin.directory.user.security", "label": "Manage user security settings" },
|
||||
{ "value": "https://www.googleapis.com/auth/admin.reports.audit.readonly", "label": "Read audit logs (login, admin, Drive, token activity)" }
|
||||
],
|
||||
"extra_params": {
|
||||
"access_type": "offline",
|
||||
@@ -200,6 +254,14 @@
|
||||
"type": "web_server"
|
||||
}
|
||||
},
|
||||
"notion": {
|
||||
"auth_url": "https://api.notion.com/v1/oauth/authorize",
|
||||
"token_url": "https://api.notion.com/v1/oauth/token",
|
||||
"scopes": [],
|
||||
"extra_params": {
|
||||
"owner": "user"
|
||||
}
|
||||
},
|
||||
"linkedin": {
|
||||
"auth_url": "https://www.linkedin.com/oauth/v2/authorization",
|
||||
"token_url": "https://www.linkedin.com/oauth/v2/accessToken",
|
||||
|
||||
+24
-24
@@ -6216,7 +6216,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -6302,7 +6302,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -6314,7 +6314,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"serde",
|
||||
@@ -6323,7 +6323,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6335,7 +6335,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6347,7 +6347,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -6359,7 +6359,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6371,7 +6371,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6383,7 +6383,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -6394,7 +6394,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -6405,7 +6405,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -6417,7 +6417,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-asset"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -6428,7 +6428,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -6450,7 +6450,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-r"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6462,7 +6462,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6476,7 +6476,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case",
|
||||
@@ -6493,7 +6493,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6506,7 +6506,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql-asset"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -6518,7 +6518,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6536,7 +6536,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts-asset"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde-wasm-bindgen",
|
||||
@@ -6552,7 +6552,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wac"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -6568,7 +6568,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wasm"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"getrandom 0.2.17",
|
||||
@@ -6600,7 +6600,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6614,7 +6614,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-types"
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
|
||||
@@ -12,7 +12,7 @@ resolver = "2"
|
||||
members = ["."]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.820.0"
|
||||
version = "1.825.0"
|
||||
edition = "2021"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
|
||||
|
||||
+39
-39
@@ -44,28 +44,27 @@ use windmill_common::{
|
||||
CRITICAL_ALERTS_ON_TOKEN_EXPIRY_SETTING, CRITICAL_ALERT_MUTE_UI_SETTING,
|
||||
CRITICAL_ALERT_MUTE_ZOMBIE_JOB_RESTART_SETTING, CRITICAL_ERROR_CHANNELS_SETTING,
|
||||
CUSTOM_TAGS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING,
|
||||
DISABLE_PASSWORD_LOGIN_SETTING, EMAIL_DOMAIN_SETTING, ENV_SETTINGS,
|
||||
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
|
||||
FORK_WORKSPACE_TAG_APPEND_FORK_SUFFIX_SETTING,
|
||||
DEPENDENCY_JOB_TAG_SETTING, DISABLE_PASSWORD_LOGIN_SETTING, EMAIL_DOMAIN_SETTING,
|
||||
ENV_SETTINGS, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING,
|
||||
EXTRA_PIP_INDEX_URL_SETTING, FORK_WORKSPACE_TAG_APPEND_FORK_SUFFIX_SETTING,
|
||||
HTTP_ROUTE_DEFAULT_ALLOWED_ORIGINS_SETTING, HTTP_ROUTE_WORKSPACED_ROUTE_SETTING,
|
||||
HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING, INDEXER_SETTING,
|
||||
INSTANCE_EVENTS_WEBHOOK_SETTING, INSTANCE_PYTHON_VERSION_SETTING,
|
||||
JOB_DEFAULT_TIMEOUT_SECS_SETTING, JOB_ISOLATION_SETTING, JWT_SECRET_SETTING,
|
||||
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MAVEN_REPOS_SETTING, MAVEN_SETTINGS_XML_SETTING,
|
||||
MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING,
|
||||
NO_DEFAULT_MAVEN_SETTING,
|
||||
NPM_CONFIG_REGISTRY_SETTING, NSJAIL_TMPFS_SIZE_MB_SETTING, NSJAIL_TMP_BACKING_SETTING,
|
||||
NUGET_CONFIG_SETTING, OAUTH_SETTING, OTEL_SETTING, OTEL_TRACES_RETENTION_SECS_SETTING,
|
||||
OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING,
|
||||
POWERSHELL_REPO_URL_SETTING, PREVIEW_TAGS_OVERRIDE_SETTING, REQUEST_SIZE_LIMIT_SETTING,
|
||||
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RESTART_COORDINATION_SETTING,
|
||||
RETENTION_PERIOD_SECS_OVERRIDES_SETTING, RETENTION_PERIOD_SECS_SETTING, RUBY_REPOS_SETTING,
|
||||
SAML_METADATA_SETTING, SANDBOX_IMAGE_CACHE_MAX_MB_SETTING,
|
||||
SANDBOX_IMAGE_DEFAULT_REGISTRY_SETTING, SANDBOX_IMAGE_MAX_SIZE_MB_SETTING,
|
||||
SANDBOX_IMAGE_PULL_POLICY_SETTING, SANDBOX_REGISTRY_AUTH_SETTING, SCIM_TOKEN_SETTING,
|
||||
SERVICE_LOG_RETENTION_SECS_SETTING, SMTP_SETTING, STORE_AUDIT_LOGS_S3_SETTING,
|
||||
TEAMS_SETTING, TIMEOUT_WAIT_RESULT_SETTING, UV_EXCLUDE_NEWER_SETTING,
|
||||
UV_INDEX_STRATEGY_SETTING, UV_PYTHON_INSTALL_MIRROR_SETTING,
|
||||
NO_DEFAULT_MAVEN_SETTING, NPM_CONFIG_REGISTRY_SETTING, NSJAIL_TMPFS_SIZE_MB_SETTING,
|
||||
NSJAIL_TMP_BACKING_SETTING, NUGET_CONFIG_SETTING, OAUTH_SETTING, OTEL_SETTING,
|
||||
OTEL_TRACES_RETENTION_SECS_SETTING, OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING,
|
||||
POWERSHELL_REPO_PAT_SETTING, POWERSHELL_REPO_URL_SETTING, PREVIEW_TAGS_OVERRIDE_SETTING,
|
||||
REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING,
|
||||
RESTART_COORDINATION_SETTING, RETENTION_PERIOD_SECS_OVERRIDES_SETTING,
|
||||
RETENTION_PERIOD_SECS_SETTING, RUBY_REPOS_SETTING, SAML_METADATA_SETTING,
|
||||
SANDBOX_IMAGE_CACHE_MAX_MB_SETTING, SANDBOX_IMAGE_DEFAULT_REGISTRY_SETTING,
|
||||
SANDBOX_IMAGE_MAX_SIZE_MB_SETTING, SANDBOX_IMAGE_PULL_POLICY_SETTING,
|
||||
SANDBOX_REGISTRY_AUTH_SETTING, SCIM_TOKEN_SETTING, SERVICE_LOG_RETENTION_SECS_SETTING,
|
||||
SMTP_SETTING, STORE_AUDIT_LOGS_S3_SETTING, TEAMS_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
|
||||
UV_EXCLUDE_NEWER_SETTING, UV_INDEX_STRATEGY_SETTING, UV_PYTHON_INSTALL_MIRROR_SETTING,
|
||||
WORKSPACE_FAIRNESS_DURATION_SECS_SETTING, WORKSPACE_FAIRNESS_ENABLED_SETTING,
|
||||
WORKSPACE_FAIRNESS_MAX_PERCENT_SETTING, WORKSPACE_FAIRNESS_MIN_TOTAL_SETTING,
|
||||
WORKSPACE_MAX_QUEUED_JOBS_SETTING, WORKSPACE_REGISTRIES_SETTING,
|
||||
@@ -126,24 +125,23 @@ use windmill_worker::{
|
||||
};
|
||||
|
||||
use crate::monitor::{
|
||||
initial_load, load_concurrency_key_max_queued, load_disable_password_login,
|
||||
load_fork_workspace_tag_append_fork_suffix, load_keep_job_dir,
|
||||
load_mcp_disable_token_query_param, load_metrics_debug_enabled,
|
||||
load_preview_tags_override, load_require_preexisting_user, load_retention_period_overrides,
|
||||
load_tag_per_workspace_enabled, load_tag_per_workspace_workspaces,
|
||||
load_workspace_fairness_duration_secs, load_workspace_fairness_enabled,
|
||||
load_workspace_fairness_max_percent, load_workspace_fairness_min_total,
|
||||
load_workspace_max_queued_jobs, monitor_db, reload_app_workspaced_route_setting,
|
||||
reload_audit_log_retention_days_setting, reload_base_url_setting,
|
||||
reload_bun_install_min_release_age_setting, reload_bunfig_install_scopes_setting,
|
||||
reload_critical_alert_mute_ui_setting, reload_critical_alert_mute_zombie_job_restart_setting,
|
||||
initial_load, load_concurrency_key_max_queued, load_dependency_job_tag,
|
||||
load_disable_password_login, load_fork_workspace_tag_append_fork_suffix, load_keep_job_dir,
|
||||
load_mcp_disable_token_query_param, load_metrics_debug_enabled, load_preview_tags_override,
|
||||
load_require_preexisting_user, load_retention_period_overrides, load_tag_per_workspace_enabled,
|
||||
load_tag_per_workspace_workspaces, load_workspace_fairness_duration_secs,
|
||||
load_workspace_fairness_enabled, load_workspace_fairness_max_percent,
|
||||
load_workspace_fairness_min_total, load_workspace_max_queued_jobs, monitor_db,
|
||||
reload_app_workspaced_route_setting, reload_audit_log_retention_days_setting,
|
||||
reload_base_url_setting, reload_bun_install_min_release_age_setting,
|
||||
reload_bunfig_install_scopes_setting, reload_critical_alert_mute_ui_setting,
|
||||
reload_critical_alert_mute_zombie_job_restart_setting,
|
||||
reload_critical_alerts_on_token_expiry_setting, reload_critical_error_channels_setting,
|
||||
reload_extra_pip_index_url_setting, reload_http_route_default_allowed_origins_setting,
|
||||
reload_http_route_workspaced_route_setting,
|
||||
reload_hub_api_secret_setting, reload_hub_base_url_setting,
|
||||
reload_instance_events_webhook_setting, reload_job_default_timeout_setting,
|
||||
reload_job_isolation_setting, reload_jwt_secret_setting, reload_license_key,
|
||||
reload_npm_config_registry_setting, reload_nsjail_tmp_backing_setting,
|
||||
reload_http_route_workspaced_route_setting, reload_hub_api_secret_setting,
|
||||
reload_hub_base_url_setting, reload_instance_events_webhook_setting,
|
||||
reload_job_default_timeout_setting, reload_job_isolation_setting, reload_jwt_secret_setting,
|
||||
reload_license_key, reload_npm_config_registry_setting, reload_nsjail_tmp_backing_setting,
|
||||
reload_nsjail_tmpfs_size_setting, reload_otel_traces_retention_secs_setting,
|
||||
reload_otel_tracing_proxy_setting, reload_pip_index_url_setting,
|
||||
reload_retention_period_setting, reload_sandbox_image_cache_max_setting,
|
||||
@@ -423,7 +421,6 @@ struct HubResourceTypeRaw {
|
||||
pub display_name: Option<Option<String>>,
|
||||
}
|
||||
|
||||
|
||||
/// Processed resource type with parsed schema
|
||||
#[derive(serde::Deserialize, serde::Serialize, Clone)]
|
||||
pub struct HubResourceType {
|
||||
@@ -1889,12 +1886,10 @@ async fn process_notify_event(
|
||||
);
|
||||
windmill_common::workspaces::PUBLIC_APP_RATE_LIMIT_CACHE.remove(payload);
|
||||
}
|
||||
windmill_common::SCRIPT_VERSION_DELETED_CHANNEL => {
|
||||
match serde_json::from_str(payload) {
|
||||
Ok(deleted) => windmill_api_scripts::scripts::evict_deleted_script_versions(deleted),
|
||||
Err(e) => tracing::error!("Invalid script version deletion payload {payload}: {e}"),
|
||||
}
|
||||
}
|
||||
windmill_common::SCRIPT_VERSION_DELETED_CHANNEL => match serde_json::from_str(payload) {
|
||||
Ok(deleted) => windmill_api_scripts::scripts::evict_deleted_script_versions(deleted),
|
||||
Err(e) => tracing::error!("Invalid script version deletion payload {payload}: {e}"),
|
||||
},
|
||||
"notify_runnable_version_change" => {
|
||||
tracing::info!("Runnable version change detected: {}", payload);
|
||||
match payload.split(':').collect::<Vec<&str>>().as_slice() {
|
||||
@@ -2055,6 +2050,11 @@ async fn process_notify_event(
|
||||
tracing::error!("Error loading preview tags override: {e:#}");
|
||||
}
|
||||
}
|
||||
DEPENDENCY_JOB_TAG_SETTING => {
|
||||
if let Err(e) = load_dependency_job_tag(db).await {
|
||||
tracing::error!("Error loading dependency job tag: {e:#}");
|
||||
}
|
||||
}
|
||||
WORKSPACE_FAIRNESS_ENABLED_SETTING => {
|
||||
if let Err(e) = load_workspace_fairness_enabled(db).await {
|
||||
tracing::error!("Error loading workspace fairness enabled: {e:#}");
|
||||
|
||||
+56
-18
@@ -60,17 +60,17 @@ use windmill_common::{
|
||||
CRITICAL_ALERTS_ON_DB_OVERSIZE_SETTING, CRITICAL_ALERTS_ON_TOKEN_EXPIRY_SETTING,
|
||||
CRITICAL_ALERT_MUTE_UI_SETTING, CRITICAL_ALERT_MUTE_ZOMBIE_JOB_RESTART_SETTING,
|
||||
CRITICAL_ERROR_CHANNELS_SETTING, CUSTOM_TAGS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING,
|
||||
DEFAULT_TAGS_WORKSPACES_SETTING, DISABLE_PASSWORD_LOGIN, DISABLE_PASSWORD_LOGIN_SETTING,
|
||||
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
|
||||
FORK_WORKSPACE_TAG_APPEND_FORK_SUFFIX_SETTING, HUB_API_SECRET_SETTING,
|
||||
HUB_BASE_URL_SETTING, INSTANCE_PYTHON_VERSION_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING,
|
||||
JOB_ISOLATION_SETTING, JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING,
|
||||
MCP_DISABLE_TOKEN_QUERY_PARAM, MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING,
|
||||
MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NPMRC_SETTING, NPM_CONFIG_REGISTRY_SETTING,
|
||||
NSJAIL_TMPFS_SIZE_MB_SETTING, NSJAIL_TMP_BACKING_SETTING, NUGET_CONFIG_SETTING,
|
||||
OTEL_SETTING, OTEL_TRACES_RETENTION_SECS_SETTING, OTEL_TRACING_PROXY_SETTING,
|
||||
PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING, POWERSHELL_REPO_URL_SETTING,
|
||||
PREVIEW_TAGS_OVERRIDE_SETTING, REQUEST_SIZE_LIMIT_SETTING,
|
||||
DEFAULT_TAGS_WORKSPACES_SETTING, DEPENDENCY_JOB_TAG_SETTING, DISABLE_PASSWORD_LOGIN,
|
||||
DISABLE_PASSWORD_LOGIN_SETTING, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING,
|
||||
EXTRA_PIP_INDEX_URL_SETTING, FORK_WORKSPACE_TAG_APPEND_FORK_SUFFIX_SETTING,
|
||||
HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING, INSTANCE_PYTHON_VERSION_SETTING,
|
||||
JOB_DEFAULT_TIMEOUT_SECS_SETTING, JOB_ISOLATION_SETTING, JWT_SECRET_SETTING,
|
||||
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MCP_DISABLE_TOKEN_QUERY_PARAM,
|
||||
MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NPMRC_SETTING,
|
||||
NPM_CONFIG_REGISTRY_SETTING, NSJAIL_TMPFS_SIZE_MB_SETTING, NSJAIL_TMP_BACKING_SETTING,
|
||||
NUGET_CONFIG_SETTING, OTEL_SETTING, OTEL_TRACES_RETENTION_SECS_SETTING,
|
||||
OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING,
|
||||
POWERSHELL_REPO_URL_SETTING, PREVIEW_TAGS_OVERRIDE_SETTING, REQUEST_SIZE_LIMIT_SETTING,
|
||||
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
|
||||
SAML_METADATA_SETTING, SANDBOX_IMAGE_CACHE_MAX_MB_SETTING,
|
||||
SANDBOX_IMAGE_DEFAULT_REGISTRY_SETTING, SANDBOX_IMAGE_MAX_SIZE_MB_SETTING,
|
||||
@@ -93,11 +93,11 @@ use windmill_common::{
|
||||
load_periodic_bash_script_interval_from_env, load_whitelist_env_vars_from_env,
|
||||
load_worker_config, store_pull_query, store_suspended_pull_query, Connection, WorkerConfig,
|
||||
CLOUD_HOSTED, CONCURRENCY_KEY_MAX_QUEUED, CONCURRENCY_KEY_MAX_QUEUED_DEFAULT,
|
||||
DEFAULT_TAGS_PER_WORKSPACE, DEFAULT_TAGS_WORKSPACES, FORK_WORKSPACE_TAG_APPEND_FORK_SUFFIX,
|
||||
INDEXER_CONFIG, PREVIEW_TAGS_OVERRIDE, SMTP_CONFIG, WINDMILL_DIR, WORKER_CONFIG,
|
||||
WORKER_GROUP, WORKSPACE_FAIRNESS_DURATION_SECS, WORKSPACE_FAIRNESS_ENABLED,
|
||||
WORKSPACE_FAIRNESS_MAX_PERCENT, WORKSPACE_FAIRNESS_MIN_TOTAL, WORKSPACE_MAX_QUEUED_JOBS,
|
||||
WORKSPACE_MAX_QUEUED_JOBS_DEFAULT,
|
||||
DEFAULT_TAGS_PER_WORKSPACE, DEFAULT_TAGS_WORKSPACES, DEPENDENCY_JOB_TAG,
|
||||
FORK_WORKSPACE_TAG_APPEND_FORK_SUFFIX, INDEXER_CONFIG, PREVIEW_TAGS_OVERRIDE, SMTP_CONFIG,
|
||||
WINDMILL_DIR, WORKER_CONFIG, WORKER_GROUP, WORKSPACE_FAIRNESS_DURATION_SECS,
|
||||
WORKSPACE_FAIRNESS_ENABLED, WORKSPACE_FAIRNESS_MAX_PERCENT, WORKSPACE_FAIRNESS_MIN_TOTAL,
|
||||
WORKSPACE_MAX_QUEUED_JOBS, WORKSPACE_MAX_QUEUED_JOBS_DEFAULT,
|
||||
},
|
||||
KillpillSender, AUDIT_LOG_RETENTION_DAYS, BASE_URL, CRITICAL_ALERTS_ON_DB_OVERSIZE,
|
||||
CRITICAL_ALERTS_ON_TOKEN_EXPIRY, CRITICAL_ALERT_MUTE_UI_ENABLED,
|
||||
@@ -312,6 +312,9 @@ pub async fn initial_load(
|
||||
pass.setting(PREVIEW_TAGS_OVERRIDE_SETTING, false, |v| async move {
|
||||
apply_preview_tags_override(v)
|
||||
});
|
||||
pass.setting(DEPENDENCY_JOB_TAG_SETTING, false, |v| async move {
|
||||
apply_dependency_job_tag(v)
|
||||
});
|
||||
|
||||
// Load per-workspace retention overrides before the first cleanup tick so a fresh server
|
||||
// never sweeps globally without honoring configured longer-retention workspaces.
|
||||
@@ -876,6 +879,24 @@ pub fn apply_preview_tags_override(value: Option<serde_json::Value>) {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn load_dependency_job_tag(db: &DB) -> error::Result<()> {
|
||||
let v = load_value_from_global_settings(db, DEPENDENCY_JOB_TAG_SETTING).await?;
|
||||
apply_dependency_job_tag(v);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_dependency_job_tag(value: Option<serde_json::Value>) {
|
||||
let tag = match value {
|
||||
Some(serde_json::Value::String(t)) if !t.trim().is_empty() => Some(t.trim().to_string()),
|
||||
None | Some(serde_json::Value::Null) | Some(serde_json::Value::String(_)) => None,
|
||||
Some(other) => {
|
||||
tracing::error!("{DEPENDENCY_JOB_TAG_SETTING} is not a string: {other}, ignoring");
|
||||
return;
|
||||
}
|
||||
};
|
||||
DEPENDENCY_JOB_TAG.store(std::sync::Arc::new(tag));
|
||||
}
|
||||
|
||||
// Upper bound on the duration window. Postgres `make_interval(secs => $1::int4)` is the consumer
|
||||
// downstream, so this stays comfortably below `i32::MAX` and the subsequent `u32 -> i32` cast in
|
||||
// `workspace_fairness::refresh_overloaded` cannot wrap into a negative interval (which would
|
||||
@@ -6146,6 +6167,21 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, node_n
|
||||
continue;
|
||||
}
|
||||
if let Some(job) = job.unwrap() {
|
||||
// Read while the job is still queued: a re-run (perpetual, retry) takes its cap from
|
||||
// here once the `job_perms` row is swept after completion. A failed read leaves the
|
||||
// job for the next sweep rather than completing it with no cap.
|
||||
let perms =
|
||||
match windmill_common::auth::get_job_perms(db, &job.id, &job.workspace_id).await {
|
||||
Ok(perms) => perms,
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Could not read the permissions of zombie job {}: {e:#}",
|
||||
job.id
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let job_token_scopes = perms.as_ref().and_then(|p| p.job_token_scopes.clone());
|
||||
let label = ephemeral_script_token_label(&job.permissioned_as, &job.created_by);
|
||||
let token = create_token_for_owner(
|
||||
&db,
|
||||
@@ -6155,7 +6191,7 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, node_n
|
||||
job_token_expiry_secs(&db, &job.workspace_id).await,
|
||||
&job.permissioned_as_email,
|
||||
&job.id,
|
||||
None,
|
||||
perms,
|
||||
Some(format!("handle_zombie_jobs")),
|
||||
)
|
||||
.await
|
||||
@@ -6177,10 +6213,12 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, node_n
|
||||
);
|
||||
let memory_peak = job.memory_peak.unwrap_or(0);
|
||||
let (_, killpill_rx_never_used) = KillpillSender::new(1);
|
||||
let mut completed = windmill_queue::MiniCompletedJob::from(job);
|
||||
completed.job_token_scopes = job_token_scopes;
|
||||
let _ = handle_job_error(
|
||||
db,
|
||||
&client,
|
||||
&windmill_queue::MiniCompletedJob::from(job),
|
||||
&completed,
|
||||
memory_peak,
|
||||
None,
|
||||
error::Error::ExecutionErr(error_message.clone()),
|
||||
|
||||
@@ -97,7 +97,7 @@ draft: workspace_id(char), path(char), typ(draft_type), value(json), created_at(
|
||||
email_to_igroup: email(char), igroup(char)
|
||||
email_trigger: path(char), local_part(char), workspaced_local_part(bool), script_path(char), is_flow(bool), workspace_id(char), edited_by(char), email(char), edited_at(ts), extra_perms(jsonb), error_handler_path(char), error_handler_args(jsonb), retry(jsonb), mode(trigger_mode), labels(text[])
|
||||
favorite: usr(char), workspace_id(char), path(char), favorite_kind(favorite_kind)
|
||||
flow: workspace_id(char), path(char), summary(text), description(text), value(jsonb), edited_by(char), edited_at(ts), archived(bool), schema(json), extra_perms(jsonb), dependency_job(uuid), draft_only(bool), tag(char), ws_error_handler_muted(bool), dedicated_worker(bool), timeout(int), visible_to_runner_only(bool), concurrency_key(char), versions(bigint[]), on_behalf_of(varchar), on_behalf_of_email(text), lock_error_logs(text), labels(text[])
|
||||
flow: workspace_id(char), path(char), summary(text), description(text), value(jsonb), edited_by(char), edited_at(ts), archived(bool), schema(json), extra_perms(jsonb), dependency_job(uuid), draft_only(bool), tag(char), ws_error_handler_muted(bool), dedicated_worker(bool), timeout(int), visible_to_runner_only(bool), concurrency_key(char), versions(bigint[]), on_behalf_of(varchar), on_behalf_of_email(text), lock_error_logs(text), labels(text[]), job_token_scopes(text[])
|
||||
FK: (workspace_id) -> workspace(id)
|
||||
flow_conversation: id(uuid), workspace_id(char), flow_path(char), title(char), created_at(ts), updated_at(ts), created_by(char), is_test(bool)
|
||||
FK: (workspace_id) -> workspace(id)
|
||||
@@ -132,7 +132,7 @@ input: id(uuid), workspace_id(char), runnable_id(char), runnable_type(runnable_t
|
||||
FK: (workspace_id) -> workspace(id)
|
||||
instance_group: name(char), summary(char), id(char), scim_display_name(char), external_id(char)
|
||||
job_logs: job_id(uuid), workspace_id(char), created_at(ts), logs(text), log_offset(int), log_file_index(text[])
|
||||
job_perms: job_id(uuid), email(char), username(char), is_admin(bool), is_operator(bool), created_at(ts), workspace_id(char), groups(text[]), folders(jsonb[]), end_user_email(char)
|
||||
job_perms: job_id(uuid), email(char), username(char), is_admin(bool), is_operator(bool), created_at(ts), workspace_id(char), groups(text[]), folders(jsonb[]), end_user_email(char), job_token_scopes(text[])
|
||||
job_resolution: job_id(uuid), workspace_id(char), resolved_at(ts), resolved_by(char), note(text), automatic(bool)
|
||||
job_result_stream: job_id(uuid), workspace_id(text), stream(text)
|
||||
job_result_stream_v2: job_id(uuid), workspace_id(text), stream(text), idx(int)
|
||||
@@ -183,7 +183,7 @@ resume_job: id(uuid), job(uuid), flow(uuid), created_at(ts), value(jsonb), appro
|
||||
runnable_settings: hash(bigint), debouncing_settings(bigint), concurrency_settings(bigint)
|
||||
schedule: workspace_id(char), path(char), edited_by(char), edited_at(ts), schedule(char), enabled(bool), script_path(char), args(jsonb), extra_perms(jsonb), is_flow(bool), email(char), error(text), timezone(char), on_failure(char), on_recovery(char), on_failure_times(int), on_failure_exact(bool), on_failure_extra_args(jsonb), on_recovery_times(int), on_recovery_extra_args(jsonb), ws_error_handler_muted(bool), retry(jsonb), summary(char), no_flow_overlap(bool), tag(char), paused_until(ts), on_success(char), on_success_extra_args(jsonb), cron_version(text), description(text), dynamic_skip(char), labels(text[])
|
||||
FK: (workspace_id) -> workspace(id)
|
||||
script: workspace_id(char), hash(bigint), path(char), parent_hashes(bigint[]), summary(text), description(text), content(text), created_by(char), created_at(ts), archived(bool), schema(json), deleted(bool), is_template(bool), extra_perms(jsonb), lock(text), lock_error_logs(text), language(script_lang), kind(script_kind), tag(char), draft_only(bool), envs(char), concurrent_limit(int), concurrency_time_window_s(int), cache_ttl(int), dedicated_worker(bool), ws_error_handler_muted(bool), priority(smallint), timeout(int), delete_after_use(bool), restart_unless_cancelled(bool), concurrency_key(char), visible_to_runner_only(bool), auto_kind(varchar), codebase(char), has_preprocessor(bool), schema_validation(bool), assets(jsonb), debounce_key(char), debounce_delay_s(int), cache_ignore_s3_path(bool), runnable_settings_handle(bigint), labels(text[]), on_behalf_of(varchar), on_behalf_of_email(text)
|
||||
script: workspace_id(char), hash(bigint), path(char), parent_hashes(bigint[]), summary(text), description(text), content(text), created_by(char), created_at(ts), archived(bool), schema(json), deleted(bool), is_template(bool), extra_perms(jsonb), lock(text), lock_error_logs(text), language(script_lang), kind(script_kind), tag(char), draft_only(bool), envs(char), concurrent_limit(int), concurrency_time_window_s(int), cache_ttl(int), dedicated_worker(bool), ws_error_handler_muted(bool), priority(smallint), timeout(int), delete_after_use(bool), restart_unless_cancelled(bool), concurrency_key(char), visible_to_runner_only(bool), auto_kind(varchar), codebase(char), has_preprocessor(bool), schema_validation(bool), assets(jsonb), debounce_key(char), debounce_delay_s(int), cache_ignore_s3_path(bool), runnable_settings_handle(bigint), labels(text[]), on_behalf_of(varchar), on_behalf_of_email(text), job_token_scopes(text[])
|
||||
FK: (workspace_id) -> workspace(id)
|
||||
skip_workspace_diff_tally: workspace_id(char), added_at(ts)
|
||||
sqs_trigger: path(char), queue_url(char), aws_resource_path(char), message_attributes(text[]), script_path(char), is_flow(bool), workspace_id(char), edited_by(char), email(char), edited_at(ts), extra_perms(jsonb), error(text), server_id(char), last_server_ping(ts), aws_auth_resource_type(aws_auth_resource_type), error_handler_path(char), error_handler_args(jsonb), retry(jsonb), mode(trigger_mode), labels(text[])
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
use axum::{routing::post, Json, Router};
|
||||
use serde_json::{json, Value};
|
||||
use sqlx::{Pool, Postgres};
|
||||
use std::time::Duration;
|
||||
use windmill_common::{flows::FlowValue, jobs::JobPayload};
|
||||
use windmill_test_utils::{
|
||||
completed_job, in_test_worker, listen_for_completed_jobs, ApiServer, RunJob, StreamFind,
|
||||
};
|
||||
|
||||
const MEMORY_ID: &str = "0199c0de-0000-7000-8000-000000000001";
|
||||
|
||||
/// A model that puts a U+0000 in its tool call arguments, then in its answer.
|
||||
async fn model(Json(body): Json<Value>) -> ([(&'static str, &'static str); 1], String) {
|
||||
let finished = body["messages"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|m| m["role"] == "tool");
|
||||
let delta = if finished {
|
||||
json!({"role":"assistant", "content":"are\u{0}tired"})
|
||||
} else {
|
||||
json!({"role":"assistant", "tool_calls":[{
|
||||
"index":0, "id":"call_0", "type":"function",
|
||||
"function":{"name":"echo", "arguments":"{\"x\":\"a\\u0000b\"}"}
|
||||
}]})
|
||||
};
|
||||
let event = json!({"choices":[{"index":0, "delta":delta, "finish_reason":null}]});
|
||||
let end = json!({"choices":[{"index":0, "delta":{}, "finish_reason":if finished {"stop"} else {"tool_calls"}}]});
|
||||
(
|
||||
[("content-type", "text/event-stream")],
|
||||
format!("data: {event}\n\ndata: {end}\n\ndata: [DONE]\n\n"),
|
||||
)
|
||||
}
|
||||
|
||||
async fn run_flow(db: &Pool<Postgres>, modules: Value) -> anyhow::Result<Value> {
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let flow: FlowValue = serde_json::from_value(json!({ "modules": modules }))?;
|
||||
let id = RunJob::from(JobPayload::RawFlow {
|
||||
value: flow,
|
||||
path: Some("u/test/nul".into()),
|
||||
restarted_from: None,
|
||||
})
|
||||
.push(db)
|
||||
.await;
|
||||
let notifications = listen_for_completed_jobs(db).await;
|
||||
tokio::time::timeout(
|
||||
Duration::from_secs(45),
|
||||
in_test_worker(db, notifications.find(&id), server.addr.port()),
|
||||
)
|
||||
.await?;
|
||||
let job = completed_job(id, db).await;
|
||||
server.close().await?;
|
||||
assert!(job.success, "{:?}", job.result);
|
||||
Ok(job.json_result().expect("flow result"))
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
#[serial_test::serial]
|
||||
async fn agent_step_survives_nul_in_model_output(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
std::env::set_var("ALLOW_PRIVATE_AI_BASE_URLS", "true");
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
|
||||
let base = format!("http://{}", listener.local_addr()?);
|
||||
let stub = tokio::spawn(async move {
|
||||
let router = Router::new().route("/v1/chat/completions", post(model));
|
||||
axum::serve(listener, router).await.unwrap();
|
||||
});
|
||||
let result = run_flow(
|
||||
&db,
|
||||
json!([{"id":"agent","value":{
|
||||
"type":"aiagent",
|
||||
"tools":[{"id":"t0","summary":"echo","value":{
|
||||
"type":"rawscript", "language":"bun", "input_transforms":{},
|
||||
"content":"export async function main(x: string) { return x }"
|
||||
}}],
|
||||
"input_transforms":{
|
||||
"provider":{"type":"static","value":{"kind":"customai","model":"nul-test","resource":{"base_url":format!("{base}/v1")}}},
|
||||
"user_message":{"type":"static","value":"echo"},
|
||||
"memory":{"type":"static","value":{"kind":"auto","context_length":10,"memory_id":MEMORY_ID}}
|
||||
}
|
||||
}}]),
|
||||
)
|
||||
.await?;
|
||||
stub.abort();
|
||||
assert_eq!(result["output"], "aretired");
|
||||
let tool_message = result["messages"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|m| m["role"] == "tool")
|
||||
.expect("tool message");
|
||||
assert_eq!(tool_message["content"], "\"ab\"");
|
||||
// The model's own words are also written to a jsonb column the job result never passes through.
|
||||
let memory: String = sqlx::query_scalar(
|
||||
"SELECT messages::text FROM ai_agent_memory WHERE conversation_id = $1::uuid",
|
||||
)
|
||||
.bind(MEMORY_ID)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
assert!(memory.contains("aretired"), "{memory}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A step's args are evaluated from the previous step's result as the worker holds it, not as
|
||||
/// the completed job stored it.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
#[serial_test::serial]
|
||||
async fn flow_step_args_survive_nul_in_previous_result(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
let result = run_flow(
|
||||
&db,
|
||||
json!([
|
||||
{"id":"a","value":{
|
||||
"type":"rawscript", "language":"bun", "input_transforms":{},
|
||||
"content":"export async function main() { return 'a\\u0000b' }"
|
||||
}},
|
||||
{"id":"b","value":{
|
||||
"type":"rawscript", "language":"bun",
|
||||
"input_transforms":{"x":{"type":"javascript","expr":"results.a"}},
|
||||
"content":"export async function main(x: string) { return x }"
|
||||
}}
|
||||
]),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(result, "ab");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
//! AI decision steps run end to end against a stand-in for System One endpoints: TypeSafe's, and
|
||||
//! Cloudflare Workers AI's, which serves each model at its own URL inside an API envelope.
|
||||
|
||||
use axum::{routing::post, Json, Router};
|
||||
use serde_json::{json, Value};
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_common::{flows::FlowValue, jobs::JobPayload};
|
||||
use windmill_test_utils::*;
|
||||
|
||||
/// Answers every question as a choice of `refund`, the way TypeSafe shapes an answer.
|
||||
fn refund_answers(body: &Value) -> Value {
|
||||
let answers = body["questions"]
|
||||
.as_object()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|(name, _)| {
|
||||
let answer = json!({
|
||||
"type": "choice",
|
||||
"choice": "refund",
|
||||
"probabilities": {"refund": 0.9, "bug": 0.1},
|
||||
"confidence": 0.8
|
||||
});
|
||||
(name.clone(), answer)
|
||||
})
|
||||
.collect::<serde_json::Map<_, _>>();
|
||||
json!({"model": body["model"], "answers": answers, "usage": {"input_tokens": 10}})
|
||||
}
|
||||
|
||||
async fn start_decision_stub() -> anyhow::Result<u16> {
|
||||
let app = Router::new()
|
||||
.route(
|
||||
"/v1/systemone",
|
||||
post(|Json(body): Json<Value>| async move { Json(refund_answers(&body)) }),
|
||||
)
|
||||
.route(
|
||||
"/accounts/acc/ai/run/@cf/cloudflare/clef-flash",
|
||||
post(|Json(body): Json<Value>| async move {
|
||||
Json(json!({"result": refund_answers(&body), "success": true, "errors": []}))
|
||||
}),
|
||||
);
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
|
||||
let port = listener.local_addr()?.port();
|
||||
tokio::spawn(async move { axum::serve(listener, app).await });
|
||||
Ok(port)
|
||||
}
|
||||
|
||||
/// A decision's answers reach later steps as `results.<id>.output`, which a Branch to one after it
|
||||
/// routes on.
|
||||
#[cfg(feature = "quickjs")]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_ai_decision_answers_route_a_following_branch(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
// The stub listens on loopback, which the SSRF check refuses without this.
|
||||
std::env::set_var("ALLOW_PRIVATE_AI_BASE_URLS", "true");
|
||||
let stub_port = start_decision_stub().await?;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
for provider in [
|
||||
json!({
|
||||
"kind": "typesafe",
|
||||
"resource": {"api_key": "key", "base_url": format!("http://127.0.0.1:{stub_port}/v1")},
|
||||
"model": "jev-latest"
|
||||
}),
|
||||
json!({
|
||||
"kind": "cloudflare",
|
||||
"resource": {
|
||||
"token": "key",
|
||||
"base_url": format!("http://127.0.0.1:{stub_port}/accounts/acc/ai/run/@cf/cloudflare")
|
||||
},
|
||||
"model": "clef-flash"
|
||||
}),
|
||||
] {
|
||||
let flow: FlowValue = serde_json::from_value(json!({
|
||||
"modules": [
|
||||
{"id": "d", "value": {
|
||||
"type": "aidecision",
|
||||
"input_transforms": {
|
||||
"provider": {"type": "static", "value": provider},
|
||||
"state": {"type": "static", "value": "I was charged twice"},
|
||||
"questions": {"type": "static", "value": {"intent": {
|
||||
"type": "choice",
|
||||
"instructions": "What does the customer want?",
|
||||
"criteria": {"refund": "Money back", "bug": "Something is broken"}
|
||||
}}}
|
||||
}
|
||||
}},
|
||||
{"id": "route", "value": {
|
||||
"type": "branchone",
|
||||
"branches": [
|
||||
{
|
||||
"expr": "results.d.output.intent.choice === 'bug'",
|
||||
"modules": [{"id": "b", "value": {"type": "identity"}}]
|
||||
},
|
||||
{
|
||||
"expr": "results.d.output.intent.choice === 'refund'",
|
||||
"modules": [{"id": "r", "value": {
|
||||
"type": "rawscript",
|
||||
"language": "deno",
|
||||
"content": "export function main(choice){ return 'handled ' + choice }",
|
||||
"input_transforms": {"choice": {
|
||||
"type": "javascript",
|
||||
"expr": "results.d.output.intent.choice"
|
||||
}}
|
||||
}}]
|
||||
}
|
||||
],
|
||||
"default": []
|
||||
}}
|
||||
]
|
||||
}))?;
|
||||
|
||||
let job = run_job_in_new_worker_until_complete(
|
||||
&db,
|
||||
false,
|
||||
JobPayload::RawFlow { value: flow, path: None, restarted_from: None },
|
||||
port,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
job.json_result(),
|
||||
Some(json!("handled refund")),
|
||||
"{provider}"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -245,6 +245,7 @@ fn make_mini(id: Uuid, runnable_path: &str) -> MiniCompletedJob {
|
||||
cache_ignore_s3_path: None,
|
||||
runnable_settings_handle: None,
|
||||
build_binary_only: false,
|
||||
job_token_scopes: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,6 +331,7 @@ async fn end_to_end_asset_dispatch(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
};
|
||||
let completed = RunJob::from(job).run_until_complete(&db, false, port).await;
|
||||
assert!(
|
||||
@@ -493,6 +495,7 @@ async fn partition_dynamic_resolved_persisted_and_propagated(
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
};
|
||||
let completed = RunJob::from(job)
|
||||
.arg("tenant_id", json!("acme"))
|
||||
|
||||
@@ -53,6 +53,7 @@ fn script_payload() -> JobPayload {
|
||||
concurrency_settings: ConcurrencySettings::default().into(),
|
||||
debouncing_settings: DebouncingSettings::default(),
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +64,7 @@ fn flow_payload() -> JobPayload {
|
||||
apply_preprocessor: false,
|
||||
version: FLOW_VERSION,
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -854,6 +854,7 @@ fn run_main_script_job(hash: i64) -> RunJob {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ async fn stored_modules(db: &Pool<Postgres>, payload: JobPayload) -> Option<serd
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("push must succeed");
|
||||
@@ -81,6 +82,7 @@ async fn caller_modules_never_reach_a_job(db: Pool<Postgres>) {
|
||||
concurrency_settings: ConcurrencySettings::default(),
|
||||
debouncing_settings: DebouncingSettings::default(),
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
};
|
||||
assert_eq!(stored_modules(&db, deployed).await, None);
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ mod dedicated_worker_tests {
|
||||
apply_preprocessor: false,
|
||||
labels: None,
|
||||
version: 3000000000000001,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -122,6 +123,7 @@ mod dedicated_worker_tests {
|
||||
apply_preprocessor: false,
|
||||
labels: None,
|
||||
version: 3000000000000002,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(7))
|
||||
.push(&db)
|
||||
@@ -161,6 +163,7 @@ mod dedicated_worker_tests {
|
||||
apply_preprocessor: false,
|
||||
labels: None,
|
||||
version: 3000000000000003,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -205,6 +208,7 @@ mod dedicated_worker_tests {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(21))
|
||||
.push(&db)
|
||||
@@ -247,6 +251,7 @@ mod dedicated_worker_tests {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -265,6 +270,7 @@ mod dedicated_worker_tests {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -327,6 +333,7 @@ mod dedicated_worker_tests {
|
||||
apply_preprocessor: false,
|
||||
labels: None,
|
||||
version: 3000000000000004,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.run_until_complete(&db, false, port)
|
||||
.await
|
||||
@@ -356,6 +363,7 @@ mod dedicated_worker_tests {
|
||||
apply_preprocessor: false,
|
||||
labels: None,
|
||||
version: 3000000000000005,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -391,6 +399,7 @@ mod dedicated_worker_tests {
|
||||
apply_preprocessor: false,
|
||||
labels: None,
|
||||
version: 3000000000000006,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -427,6 +436,7 @@ mod dedicated_worker_tests {
|
||||
apply_preprocessor: false,
|
||||
labels: None,
|
||||
version: 3000000000000007,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -465,6 +475,7 @@ mod dedicated_worker_tests {
|
||||
apply_preprocessor: false,
|
||||
labels: None,
|
||||
version: 3000000000000008,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -500,6 +511,7 @@ mod dedicated_worker_tests {
|
||||
apply_preprocessor: false,
|
||||
labels: None,
|
||||
version: 3000000000000009,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -511,6 +523,7 @@ mod dedicated_worker_tests {
|
||||
apply_preprocessor: false,
|
||||
labels: None,
|
||||
version: 3000000000000010,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -564,6 +577,7 @@ mod dedicated_worker_tests {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -622,6 +636,7 @@ mod dedicated_worker_tests {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(7))
|
||||
.push(&db)
|
||||
@@ -634,6 +649,7 @@ mod dedicated_worker_tests {
|
||||
apply_preprocessor: false,
|
||||
labels: None,
|
||||
version: 3000000000000011,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(7))
|
||||
.push(&db)
|
||||
@@ -683,6 +699,7 @@ mod dedicated_worker_tests {
|
||||
apply_preprocessor: false,
|
||||
labels: None,
|
||||
version: 3000000000000012,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -742,6 +759,7 @@ mod dedicated_worker_tests {
|
||||
apply_preprocessor: false,
|
||||
labels: None,
|
||||
version: 3000000000000013,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.run_until_complete(&db, false, port)
|
||||
.await
|
||||
@@ -777,6 +795,7 @@ mod dedicated_worker_tests {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -795,6 +814,7 @@ mod dedicated_worker_tests {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -848,6 +868,7 @@ mod dedicated_worker_tests {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -867,6 +888,7 @@ mod dedicated_worker_tests {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -926,6 +948,7 @@ mod dedicated_worker_tests {
|
||||
apply_preprocessor: false,
|
||||
labels: None,
|
||||
version: 3000000000000014,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(20))
|
||||
.push(&db)
|
||||
@@ -938,6 +961,7 @@ mod dedicated_worker_tests {
|
||||
apply_preprocessor: false,
|
||||
labels: None,
|
||||
version: 3000000000000014,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -988,6 +1012,7 @@ mod dedicated_worker_tests {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -1038,6 +1063,7 @@ mod dedicated_worker_tests {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -1089,6 +1115,7 @@ mod dedicated_worker_tests {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -1142,6 +1169,7 @@ mod dedicated_worker_tests {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -1160,6 +1188,7 @@ mod dedicated_worker_tests {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -1222,6 +1251,7 @@ mod dedicated_worker_tests {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(7))
|
||||
.push(&db)
|
||||
@@ -1240,6 +1270,7 @@ mod dedicated_worker_tests {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.workspace("test-workspace-2")
|
||||
.arg("x", json!(7))
|
||||
@@ -1294,6 +1325,7 @@ mod dedicated_worker_tests {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -1338,6 +1370,7 @@ mod dedicated_worker_tests {
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -1375,6 +1408,7 @@ mod dedicated_worker_tests {
|
||||
apply_preprocessor: false,
|
||||
labels: None,
|
||||
version: 3000000000000015,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.push(&db)
|
||||
.await;
|
||||
@@ -1412,6 +1446,7 @@ mod dedicated_worker_tests {
|
||||
apply_preprocessor: false,
|
||||
labels: None,
|
||||
version: 3000000000000019,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.push(&db)
|
||||
.await;
|
||||
@@ -1451,6 +1486,7 @@ mod dedicated_worker_tests {
|
||||
apply_preprocessor: false,
|
||||
labels: None,
|
||||
version: 3000000000000016,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(1))
|
||||
.push(&db)
|
||||
@@ -1490,6 +1526,7 @@ mod dedicated_worker_tests {
|
||||
apply_preprocessor: false,
|
||||
labels: None,
|
||||
version: 3000000000000017,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("x", json!(5))
|
||||
.push(&db)
|
||||
@@ -1529,6 +1566,7 @@ mod dedicated_worker_tests {
|
||||
apply_preprocessor: false,
|
||||
labels: None,
|
||||
version: 3000000000000018,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.push(&db)
|
||||
.await;
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
//! Sets the process-wide `DEPENDENCY_JOB_TAG`, so it lives in its own test binary: any other
|
||||
//! test queuing a dependency job in the same process would land on the overridden tag.
|
||||
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_common::worker::DEPENDENCY_JOB_TAG;
|
||||
use windmill_dep_map::scoped_dependency_map::DependencyDependent;
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn relocks_go_to_the_configured_dependency_job_tag(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
DEPENDENCY_JOB_TAG.store(std::sync::Arc::new(Some("deps-$workspace".to_string())));
|
||||
|
||||
for (path, hash, tag) in [
|
||||
("f/tags/tagged", 7_100_001_i64, Some("default-$workspace")),
|
||||
("f/tags/plain", 7_100_002_i64, None),
|
||||
] {
|
||||
sqlx::query(
|
||||
"INSERT INTO script(workspace_id, created_by, content, schema, summary, description, path, hash, language, tag)
|
||||
VALUES ('test-workspace', 'test-user', 'def main(): pass', '{}', '', '', $1, $2, 'python3', $3)",
|
||||
)
|
||||
.bind(path)
|
||||
.bind(hash)
|
||||
.bind(tag)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
}
|
||||
let importers = ["f/tags/tagged", "f/tags/plain"]
|
||||
.map(|p| DependencyDependent {
|
||||
importer_path: p.to_string(),
|
||||
importer_kind: "script".to_string(),
|
||||
importer_node_ids: None,
|
||||
})
|
||||
.to_vec();
|
||||
windmill_dep_map::trigger_dependents::trigger_dependents_to_recompute_dependencies(
|
||||
"test-workspace",
|
||||
importers,
|
||||
None,
|
||||
None,
|
||||
"test@windmill.dev",
|
||||
"test-user",
|
||||
"u/test-user",
|
||||
&db,
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let tags: Vec<(String, String)> = sqlx::query_as(
|
||||
"SELECT runnable_path, tag FROM v2_job WHERE kind = 'dependencies' ORDER BY runnable_path",
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
tags,
|
||||
vec![
|
||||
(
|
||||
"f/tags/plain".to_string(),
|
||||
"deps-test-workspace".to_string()
|
||||
),
|
||||
(
|
||||
"f/tags/tagged".to_string(),
|
||||
"deps-test-workspace".to_string()
|
||||
),
|
||||
]
|
||||
);
|
||||
|
||||
// A binary prebuild given `auto_build_binary_tag` keeps it; an untagged one follows its
|
||||
// dependency job.
|
||||
assert_eq!(
|
||||
push_binary_prebuild(&db, Some("build-pool")).await?,
|
||||
"build-pool"
|
||||
);
|
||||
assert_eq!(
|
||||
push_binary_prebuild(&db, None).await?,
|
||||
"deps-test-workspace"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn push_binary_prebuild(db: &Pool<Postgres>, tag: Option<&str>) -> anyhow::Result<String> {
|
||||
let mut args = std::collections::HashMap::new();
|
||||
args.insert(
|
||||
"build_binary_only".to_string(),
|
||||
windmill_common::worker::to_raw_value(&true),
|
||||
);
|
||||
let (build_id, tx) = windmill_queue::push(
|
||||
db,
|
||||
windmill_queue::PushIsolationLevel::IsolatedRoot(db.clone()),
|
||||
"test-workspace",
|
||||
windmill_common::jobs::JobPayload::BuildBinary {
|
||||
path: "f/tags/plain".to_string(),
|
||||
hash: windmill_common::scripts::ScriptHash(7_100_002),
|
||||
language: windmill_common::scripts::ScriptLang::Go,
|
||||
},
|
||||
windmill_queue::PushArgs::from(&args),
|
||||
"test-user",
|
||||
"test@windmill.dev",
|
||||
"u/test-user".to_string(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
true,
|
||||
tag.map(str::to_string),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(sqlx::query_scalar("SELECT tag FROM v2_job WHERE id = $1")
|
||||
.bind(build_id)
|
||||
.fetch_one(db)
|
||||
.await?)
|
||||
}
|
||||
@@ -452,6 +452,7 @@ def main():
|
||||
ws_error_handler_muted: None,
|
||||
labels: None,
|
||||
skip_draft_deletion: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.send()
|
||||
.await
|
||||
|
||||
@@ -161,6 +161,7 @@ export async function main(path: string, email: string, job_id: string, is_flow:
|
||||
concurrency_settings: ConcurrencySettings::default(),
|
||||
debouncing_settings: DebouncingSettings::default(),
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.run_until_complete(&db, false, server.addr.port())
|
||||
.await;
|
||||
@@ -285,6 +286,7 @@ async fn test_error_handler_muted_on_script(db: Pool<Postgres>) -> anyhow::Resul
|
||||
concurrency_settings: ConcurrencySettings::default(),
|
||||
debouncing_settings: DebouncingSettings::default(),
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.run_until_complete(&db, false, server.addr.port())
|
||||
.await;
|
||||
@@ -381,6 +383,7 @@ async fn test_error_handler_not_triggered_on_success(db: Pool<Postgres>) -> anyh
|
||||
concurrency_settings: ConcurrencySettings::default(),
|
||||
debouncing_settings: DebouncingSettings::default(),
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.run_until_complete(&db, false, server.addr.port())
|
||||
.await;
|
||||
|
||||
+7
@@ -29,6 +29,13 @@ INSERT INTO token(token_hash, token_prefix, token, email, label, super_admin, sc
|
||||
ARRAY['jobs:run:flows:f/shared/flow1']
|
||||
);
|
||||
|
||||
-- A path-scoped read token for test-user-2: it reads the runs of `f/shared/flow1` only.
|
||||
INSERT INTO token(token_hash, token_prefix, token, email, label, super_admin, scopes) VALUES (
|
||||
encode(sha256('READ_SCOPED_TOKEN'::bytea), 'hex'), 'READ_SCOP', 'READ_SCOPED_TOKEN',
|
||||
'test2@windmill.dev', 'flow history token', false,
|
||||
ARRAY['jobs:read:f/shared/flow1']
|
||||
);
|
||||
|
||||
-- Same, scoped to a script. The two jobs below both run through a `singlestepflow`
|
||||
-- wrapper (native retry / scheduled runs produce these) — one wrapping a script, one
|
||||
-- wrapping a flow — so the confinement has to project each onto the runnable it wraps
|
||||
|
||||
@@ -50,6 +50,7 @@ fn flow_module(id: &str, value: FlowModuleValue) -> FlowModule {
|
||||
apply_preprocessor: None,
|
||||
pass_flow_input_directly: None,
|
||||
debouncing: None,
|
||||
job_token_scopes: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2974,6 +2975,7 @@ async fn test_flow_env_marks_sub_flows_only_without_ancestor_env(
|
||||
apply_preprocessor: false,
|
||||
version,
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.run_until_complete(&db, false, port)
|
||||
.await;
|
||||
|
||||
@@ -57,6 +57,7 @@ mod job_payload {
|
||||
language: ScriptLang::Deno,
|
||||
priority: None,
|
||||
apply_preprocessor: false,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("world", json!("foo"))
|
||||
.run_until_complete(&db, false, port)
|
||||
@@ -92,6 +93,7 @@ mod job_payload {
|
||||
debouncing_settings:
|
||||
windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.run_until_complete_with(db, false, port, |id| async move {
|
||||
let job = sqlx::query!("SELECT preprocessed FROM v2_job WHERE id = $1", id)
|
||||
@@ -435,6 +437,7 @@ mod job_payload {
|
||||
apply_preprocessor: false,
|
||||
version: 1443253234253454,
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.run_until_complete(&db, false, port)
|
||||
.await
|
||||
@@ -485,6 +488,7 @@ mod job_payload {
|
||||
apply_preprocessor: true,
|
||||
version: 1443253234253456,
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.run_until_complete_with(db, false, port, |id| async move {
|
||||
let job = sqlx::query!("SELECT preprocessed FROM v2_job WHERE id = $1", id)
|
||||
@@ -556,6 +560,7 @@ mod job_payload {
|
||||
apply_preprocessor: true,
|
||||
version: 1443253234253454,
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.run_until_complete(&db, false, port)
|
||||
.await
|
||||
@@ -1150,6 +1155,7 @@ mod job_payload {
|
||||
apply_preprocessor: true,
|
||||
version: 1443253234253454,
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.run_until_complete(db, false, port)
|
||||
.await;
|
||||
@@ -1625,6 +1631,7 @@ mod job_payload {
|
||||
debouncing_settings:
|
||||
windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("foo", json!("hello"))
|
||||
.arg("bar", json!("world"))
|
||||
@@ -1676,6 +1683,7 @@ mod job_payload {
|
||||
debouncing_settings:
|
||||
windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("foo", json!("hello"))
|
||||
.arg("bar", json!("world"))
|
||||
@@ -1727,6 +1735,7 @@ mod job_payload {
|
||||
debouncing_settings:
|
||||
windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("foo", json!("hello"))
|
||||
.arg("bar", json!("world"))
|
||||
@@ -1778,6 +1787,7 @@ mod job_payload {
|
||||
debouncing_settings:
|
||||
windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("foo", json!("hello"))
|
||||
.arg("bar", json!("world"))
|
||||
@@ -1831,6 +1841,7 @@ mod job_payload {
|
||||
debouncing_settings:
|
||||
windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("foo", json!("hello"))
|
||||
.arg("bar", json!("world"))
|
||||
|
||||
@@ -150,3 +150,104 @@ async fn flow_steps_are_latest_only_under_a_current_unrestarted_flow(db: Pool<Po
|
||||
assert_eq!(p.latest, latest, "job {job}");
|
||||
}
|
||||
}
|
||||
|
||||
/// `wmill digest` hashes a sync checkout of the same items to the same values
|
||||
/// (`cli/test/item_digest_unit.test.ts`), so a trust policy can compare the claim to git.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn digests_match_the_cli_vectors(db: Pool<Postgres>) {
|
||||
use serde_json::{json, Value};
|
||||
use windmill_common::job_provenance::RunAsType;
|
||||
|
||||
let v: Value =
|
||||
serde_json::from_str(include_str!("../../cli/test/fixtures/item_digest_vectors.json"))
|
||||
.unwrap();
|
||||
let (script, flow) = (&v["script"], &v["flow"]);
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO script (workspace_id, hash, path, content, lock, modules, language, kind, created_by, schema, summary, description)
|
||||
VALUES ('test-workspace', 777, $1, $2, $3, $4, 'bun', 'script', 'test-user', '{}', '', '')",
|
||||
)
|
||||
.bind(script["path"].as_str())
|
||||
.bind(script["content"].as_str())
|
||||
.bind(script["lock"].as_str())
|
||||
.bind(&script["modules"])
|
||||
.execute(&db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Version 889 holds step `a` by reference, which no checkout can reproduce.
|
||||
let step = &flow["value"]["modules"][0]["value"];
|
||||
let mut by_ref = flow["value"].clone();
|
||||
by_ref["modules"][0]["value"] = json!({
|
||||
"type": "flowscript", "id": 999, "language": step["language"],
|
||||
"input_transforms": step["input_transforms"],
|
||||
});
|
||||
sqlx::query(
|
||||
"INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, versions)
|
||||
VALUES ('test-workspace', $1, '', '', $2, 'test-user', ARRAY[888::bigint, 889::bigint])",
|
||||
)
|
||||
.bind(flow["path"].as_str())
|
||||
.bind(&flow["value"])
|
||||
.execute(&db)
|
||||
.await
|
||||
.unwrap();
|
||||
for (id, value) in [(888, &flow["value"]), (889, &by_ref)] {
|
||||
sqlx::query(
|
||||
"INSERT INTO flow_version (id, workspace_id, path, value, schema, created_by)
|
||||
VALUES ($1, 'test-workspace', $2, $3, '{}', 'test-user')",
|
||||
)
|
||||
.bind(id as i64)
|
||||
.bind(flow["path"].as_str())
|
||||
.bind(value)
|
||||
.execute(&db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
sqlx::query(
|
||||
"INSERT INTO flow_node (id, workspace_id, path, code, lock, hash_v2)
|
||||
VALUES (999, 'test-workspace', $1, $2, $3, 'h')",
|
||||
)
|
||||
.bind(flow["path"].as_str())
|
||||
.bind(step["content"].as_str())
|
||||
.bind(step["lock"].as_str())
|
||||
.execute(&db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO v2_job (id, workspace_id, kind, runnable_path, runnable_id, parent_job, trigger, trigger_kind, tag, created_by, permissioned_as, permissioned_as_email)
|
||||
VALUES
|
||||
('3bb0c0de-0000-4000-8000-000000000201', 'test-workspace', 'script', $1, 777, NULL, NULL, NULL, 'bun', 'test-user', 'g/all', 'group-all@windmill.dev'),
|
||||
('3bb0c0de-0000-4000-8000-000000000202', 'test-workspace', 'flow', $2, 888, NULL, 'f/digest/nightly', 'schedule', 'flow', 'test-user', 'u/test-user', 'test@windmill.dev'),
|
||||
('3bb0c0de-0000-4000-8000-000000000203', 'test-workspace', 'flowscript', $2 || '/a', 999, '3bb0c0de-0000-4000-8000-000000000202', NULL, NULL, 'gpu', 'test-user', 'u/test-user', 'test@windmill.dev'),
|
||||
('3bb0c0de-0000-4000-8000-000000000204', 'test-workspace', 'flow', $2, 889, NULL, NULL, NULL, 'flow', 'test-user', 'u/test-user', 'test@windmill.dev')",
|
||||
)
|
||||
.bind(script["path"].as_str())
|
||||
.bind(flow["path"].as_str())
|
||||
.execute(&db)
|
||||
.await
|
||||
.unwrap();
|
||||
for q in [
|
||||
"INSERT INTO worker_ping (worker, worker_instance, worker_group) VALUES ('wk-gpu', 'wk', 'gpu-group')",
|
||||
"INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, running, worker, tag)
|
||||
VALUES ('3bb0c0de-0000-4000-8000-000000000203', 'test-workspace', now(), true, 'wk-gpu', 'gpu')",
|
||||
] {
|
||||
sqlx::query(q).execute(&db).await.unwrap();
|
||||
}
|
||||
|
||||
let s = provenance(&db, "3bb0c0de-0000-4000-8000-000000000201").await;
|
||||
assert_eq!(s.digest.as_deref(), script["digest"].as_str());
|
||||
assert_eq!(s.root_digest, s.digest);
|
||||
assert_eq!(s.run_as_type, RunAsType::Group);
|
||||
assert_eq!(s.worker_group, None);
|
||||
|
||||
let step = provenance(&db, "3bb0c0de-0000-4000-8000-000000000203").await;
|
||||
assert_eq!(step.digest.as_deref(), flow["steps"]["a"].as_str());
|
||||
assert_eq!(step.root_digest.as_deref(), flow["digest"].as_str());
|
||||
assert_eq!(step.root_trigger.as_deref(), Some("f/digest/nightly"));
|
||||
assert_eq!(step.tag, "gpu");
|
||||
assert_eq!(step.worker_group.as_deref(), Some("gpu-group"));
|
||||
assert_eq!(step.run_as_type, RunAsType::User);
|
||||
|
||||
assert_eq!(provenance(&db, "3bb0c0de-0000-4000-8000-000000000204").await.digest, None);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
//! A job token restricted by `job_token_scopes` reaches only what its scopes allow, plus the
|
||||
//! runtime routes about its own job, and no job it creates holds a wider token than it.
|
||||
|
||||
use reqwest::StatusCode;
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use uuid::Uuid;
|
||||
use windmill_common::jobs::JobPayload;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
const OIDC_JOB: &str = "b0000000-0000-0000-0000-000000000001";
|
||||
const RUN_JOB: &str = "b0000000-0000-0000-0000-000000000002";
|
||||
const FLOW_JOB: &str = "b0000000-0000-0000-0000-000000000003";
|
||||
|
||||
async fn insert_job(
|
||||
db: &Pool<Postgres>,
|
||||
id: &str,
|
||||
parent: Option<&str>,
|
||||
scopes: &[&str],
|
||||
) -> anyhow::Result<()> {
|
||||
insert_job_with(db, id, parent, Some(scopes)).await
|
||||
}
|
||||
|
||||
async fn insert_job_with(
|
||||
db: &Pool<Postgres>,
|
||||
id: &str,
|
||||
parent: Option<&str>,
|
||||
scopes: Option<&[&str]>,
|
||||
) -> anyhow::Result<()> {
|
||||
let id = Uuid::parse_str(id)?;
|
||||
sqlx::query(
|
||||
"INSERT INTO v2_job (id, workspace_id, created_by, permissioned_as, permissioned_as_email,
|
||||
kind, script_lang, runnable_path, tag, parent_job)
|
||||
VALUES ($1, 'test-workspace', 'test-user-3', 'u/test-user-3', 'test3@windmill.dev',
|
||||
'script', 'deno', 'u/test-user-3/agent', 'deno', $2)",
|
||||
)
|
||||
.bind(id)
|
||||
.bind(parent.map(Uuid::parse_str).transpose()?)
|
||||
.execute(db)
|
||||
.await?;
|
||||
sqlx::query(
|
||||
"INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups,
|
||||
workspace_id, job_token_scopes)
|
||||
VALUES ($1, 'test3@windmill.dev', 'test-user-3', false, false, '{}', '{}',
|
||||
'test-workspace', $2)",
|
||||
)
|
||||
.bind(id)
|
||||
.bind(scopes)
|
||||
.execute(db)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn job_token(db: &Pool<Postgres>, id: &str) -> anyhow::Result<String> {
|
||||
Ok(windmill_common::auth::create_token_for_owner(
|
||||
db,
|
||||
"test-workspace",
|
||||
"u/test-user-3",
|
||||
"ephemeral-script",
|
||||
300,
|
||||
"test3@windmill.dev",
|
||||
&Uuid::parse_str(id)?,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn insert_script(
|
||||
db: &Pool<Postgres>,
|
||||
path: &str,
|
||||
hash: i64,
|
||||
scopes: Option<&[&str]>,
|
||||
) -> anyhow::Result<()> {
|
||||
sqlx::query(
|
||||
"INSERT INTO script (workspace_id, created_by, content, schema, summary, description,
|
||||
path, hash, language, lock, kind, job_token_scopes)
|
||||
VALUES ('test-workspace', 'test-user-3', 'export function main() {}', '{}', '', '',
|
||||
$1, $2, 'deno', '', 'script', $3)",
|
||||
)
|
||||
.bind(path)
|
||||
.bind(hash)
|
||||
.bind(scopes)
|
||||
.execute(db)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_restricted_job_token_is_confined(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
insert_script(&db, "u/test-user-3/open", 515151, None).await?;
|
||||
insert_script(
|
||||
&db,
|
||||
"u/test-user-3/oidc_only",
|
||||
525252,
|
||||
Some(&["oidc:write"]),
|
||||
)
|
||||
.await?;
|
||||
insert_job(&db, FLOW_JOB, None, &["oidc:write"]).await?;
|
||||
insert_job(&db, OIDC_JOB, Some(FLOW_JOB), &["oidc:write"]).await?;
|
||||
insert_job(&db, RUN_JOB, None, &["jobs:run"]).await?;
|
||||
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
set_jwt_secret().await;
|
||||
let base = format!(
|
||||
"http://localhost:{}/api/w/test-workspace",
|
||||
server.addr.port()
|
||||
);
|
||||
let client = reqwest::Client::new();
|
||||
let oidc_token = job_token(&db, OIDC_JOB).await?;
|
||||
|
||||
let refused = [
|
||||
client.get(format!("{base}/variables/get_value/u/test-user-3/secret")),
|
||||
client
|
||||
.post(format!("{base}/scripts/create"))
|
||||
.json(&json!({})),
|
||||
client
|
||||
.post(format!("{base}/schedules/create"))
|
||||
.json(&json!({})),
|
||||
client
|
||||
.post(format!("{base}/jobs/run/p/u/test-user-3/open"))
|
||||
.json(&json!({})),
|
||||
];
|
||||
for request in refused {
|
||||
let resp = request.bearer_auth(&oidc_token).send().await?;
|
||||
assert_eq!(
|
||||
resp.status(),
|
||||
StatusCode::FORBIDDEN,
|
||||
"{}",
|
||||
resp.text().await?
|
||||
);
|
||||
}
|
||||
let own = client
|
||||
.get(format!("{base}/jobs_u/get_root_job_id/{OIDC_JOB}"))
|
||||
.bearer_auth(&oidc_token)
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(own.status(), StatusCode::OK, "{}", own.text().await?);
|
||||
// The orchestrator reads a flow's step results with the token of the step that just ran.
|
||||
for (job, refused) in [(FLOW_JOB, false), (RUN_JOB, true)] {
|
||||
let resp = client
|
||||
.get(format!("{base}/jobs/result_by_id/{job}/a"))
|
||||
.bearer_auth(&oidc_token)
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status() == StatusCode::FORBIDDEN, refused, "{job}");
|
||||
}
|
||||
// Its progress reaches only the flow it runs in.
|
||||
for (flow, refused) in [(FLOW_JOB, false), (RUN_JOB, true)] {
|
||||
let resp = client
|
||||
.post(format!("{base}/job_metrics/set_progress/{OIDC_JOB}"))
|
||||
.bearer_auth(&oidc_token)
|
||||
.json(&json!({ "percent": 50, "flow_job_id": flow }))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status() == StatusCode::FORBIDDEN, refused, "{flow}");
|
||||
}
|
||||
|
||||
// Imported queue rows bypass push, so a restricted token cannot import, even an admin's.
|
||||
let admin_job = Uuid::parse_str("b0000000-0000-0000-0000-000000000004")?;
|
||||
sqlx::query(
|
||||
"INSERT INTO v2_job (id, workspace_id, created_by, permissioned_as, permissioned_as_email,
|
||||
kind, script_lang, runnable_path, tag)
|
||||
VALUES ($1, 'test-workspace', 'test-user', 'u/test-user', 'test@windmill.dev',
|
||||
'script', 'deno', 'u/test-user/agent', 'deno')",
|
||||
)
|
||||
.bind(admin_job)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
sqlx::query(
|
||||
"INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups,
|
||||
workspace_id, job_token_scopes)
|
||||
VALUES ($1, 'test@windmill.dev', 'test-user', true, false, '{}', '{}', 'test-workspace',
|
||||
'{jobs:write,jobs:run}')",
|
||||
)
|
||||
.bind(admin_job)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
let admin_token = windmill_common::auth::create_token_for_owner(
|
||||
&db,
|
||||
"test-workspace",
|
||||
"u/test-user",
|
||||
"ephemeral-script",
|
||||
300,
|
||||
"test@windmill.dev",
|
||||
&admin_job,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let resp = client
|
||||
.post(format!("{base}/jobs/queue/import"))
|
||||
.bearer_auth(&admin_token)
|
||||
.json(&json!([]))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
assert!(resp.text().await?.contains("cannot import"));
|
||||
// Nor can it, even an admin's, place a child in an unrelated run: the flow-run routes
|
||||
// trust that lineage.
|
||||
let resp = client
|
||||
.post(format!(
|
||||
"{base}/jobs/run/p/u/test-user-3/open?root_job={RUN_JOB}"
|
||||
))
|
||||
.bearer_auth(&admin_token)
|
||||
.json(&json!({}))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), StatusCode::CREATED, "{}", resp.text().await?);
|
||||
let child = Uuid::parse_str(&resp.text().await?)?;
|
||||
let claimed: bool = sqlx::query_scalar(
|
||||
"SELECT $2 IN (parent_job, root_job, flow_innermost_root_job) IS TRUE FROM v2_job
|
||||
WHERE id = $1",
|
||||
)
|
||||
.bind(child)
|
||||
.bind(Uuid::parse_str(RUN_JOB)?)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
assert!(!claimed);
|
||||
|
||||
// A job it starts is capped at its own scopes, intersected with the target's setting.
|
||||
let run_token = job_token(&db, RUN_JOB).await?;
|
||||
for (path, expected) in [
|
||||
("open", vec!["jobs:run".to_string()]),
|
||||
("oidc_only", vec![]),
|
||||
] {
|
||||
let resp = client
|
||||
.post(format!("{base}/jobs/run/p/u/test-user-3/{path}"))
|
||||
.bearer_auth(&run_token)
|
||||
.json(&json!({}))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), StatusCode::CREATED);
|
||||
let child = Uuid::parse_str(&resp.text().await?)?;
|
||||
let scopes: Option<Vec<String>> =
|
||||
sqlx::query_scalar("SELECT job_token_scopes FROM job_perms WHERE job_id = $1")
|
||||
.bind(child)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
assert_eq!(scopes, Some(expected), "child of {path}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_flow_steps_inherit_the_flow_restriction(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let value = json!({ "modules": [
|
||||
{ "id": "a", "value": { "type": "identity" },
|
||||
"job_token_scopes": ["oidc:write", "variables:read"] },
|
||||
{ "id": "b", "value": { "type": "identity" } },
|
||||
] });
|
||||
sqlx::query(
|
||||
"INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at,
|
||||
schema, extra_perms, versions)
|
||||
VALUES ('test-workspace', 'u/test-user/agent_flow', '', '', $1, 'test-user', now(), '{}',
|
||||
'{}', '{7171}')",
|
||||
)
|
||||
.bind(&value)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
sqlx::query(
|
||||
"INSERT INTO flow_version (id, workspace_id, path, value, schema, created_by)
|
||||
VALUES (7171, 'test-workspace', 'u/test-user/agent_flow', $1, '{}', 'test-user')",
|
||||
)
|
||||
.bind(&value)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let scopes = |v: &[&str]| Some(v.iter().map(|s| s.to_string()).collect::<Vec<_>>());
|
||||
// A step's own setting narrows the flow's restriction and never widens it; in an
|
||||
// unrestricted flow it restricts that step alone.
|
||||
for (flow_scopes, expected_a, expected_b) in [
|
||||
(scopes(&["oidc:write"]), scopes(&["oidc:write"]), scopes(&["oidc:write"])),
|
||||
(None, scopes(&["oidc:write", "variables:read"]), None),
|
||||
] {
|
||||
let flow = RunJob::from(JobPayload::Flow {
|
||||
path: "u/test-user/agent_flow".to_string(),
|
||||
dedicated_worker: None,
|
||||
apply_preprocessor: false,
|
||||
version: 7171,
|
||||
labels: None,
|
||||
job_token_scopes: flow_scopes.clone(),
|
||||
})
|
||||
.run_until_complete(&db, false, server.addr.port())
|
||||
.await;
|
||||
|
||||
let steps: Vec<(String, Option<Vec<String>>)> = sqlx::query_as(
|
||||
"SELECT j.flow_step_id, p.job_token_scopes FROM v2_job j
|
||||
JOIN job_perms p ON p.job_id = j.id
|
||||
WHERE j.parent_job = $1 ORDER BY j.flow_step_id",
|
||||
)
|
||||
.bind(flow.id)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
steps,
|
||||
vec![("a".to_string(), expected_a), ("b".to_string(), expected_b)],
|
||||
"flow scopes {flow_scopes:?}"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_deploy_without_the_field_keeps_the_restriction(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let base = format!("http://localhost:{}/api/w/test-workspace", server.addr.port());
|
||||
let client = reqwest::Client::new();
|
||||
let script = |path: &str, scopes: Option<serde_json::Value>| {
|
||||
let mut body = json!({
|
||||
"path": path, "summary": "", "description": "", "content": "echo 42", "language": "bash",
|
||||
});
|
||||
if let Some(scopes) = scopes {
|
||||
body["job_token_scopes"] = scopes;
|
||||
}
|
||||
body
|
||||
};
|
||||
let stored = |path: &'static str| {
|
||||
let db = db.clone();
|
||||
async move {
|
||||
sqlx::query_scalar::<_, Option<Vec<String>>>(
|
||||
"SELECT job_token_scopes FROM script WHERE path = $1 ORDER BY created_at DESC LIMIT 1",
|
||||
)
|
||||
.bind(path)
|
||||
.fetch_one(&db)
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
let resp = client
|
||||
.post(format!("{base}/scripts/create"))
|
||||
.bearer_auth("SECRET_TOKEN")
|
||||
.json(&script("u/test-user/agent", Some(json!(["oidc:write"]))))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), StatusCode::CREATED, "{}", resp.text().await?);
|
||||
|
||||
// A rename by a client that does not know the field carries the restriction along.
|
||||
let resp = client
|
||||
.post(format!("{base}/scripts/update/u/test-user/agent"))
|
||||
.bearer_auth("SECRET_TOKEN")
|
||||
.json(&script("u/test-user/renamed", None))
|
||||
.send()
|
||||
.await?;
|
||||
assert!(resp.status().is_success(), "{}", resp.text().await?);
|
||||
assert_eq!(stored("u/test-user/renamed").await?, Some(vec!["oidc:write".to_string()]));
|
||||
|
||||
// An explicit null clears it.
|
||||
let resp = client
|
||||
.post(format!("{base}/scripts/update/u/test-user/renamed"))
|
||||
.bearer_auth("SECRET_TOKEN")
|
||||
.json(&script("u/test-user/renamed", Some(serde_json::Value::Null)))
|
||||
.send()
|
||||
.await?;
|
||||
assert!(resp.status().is_success(), "{}", resp.text().await?);
|
||||
assert_eq!(stored("u/test-user/renamed").await?, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The OIDC `job_token_scopes` claim states the scopes of the token that asked for it.
|
||||
#[cfg(all(feature = "enterprise", feature = "private", feature = "openidconnect"))]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_oidc_claim_carries_the_presented_scopes(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
use base64::Engine;
|
||||
|
||||
initialize_tracing().await;
|
||||
const OPEN_JOB: &str = "b0000000-0000-0000-0000-000000000005";
|
||||
insert_script(&db, "u/test-user-3/agent", 535353, None).await?;
|
||||
insert_job(&db, OIDC_JOB, None, &["oidc:write"]).await?;
|
||||
insert_job_with(&db, OPEN_JOB, None, None).await?;
|
||||
sqlx::query("UPDATE v2_job SET runnable_id = 535353 WHERE id = ANY($1)")
|
||||
.bind([Uuid::parse_str(OIDC_JOB)?, Uuid::parse_str(OPEN_JOB)?])
|
||||
.execute(&db)
|
||||
.await?;
|
||||
sqlx::query(
|
||||
"INSERT INTO v2_job_queue (id, workspace_id, running, scheduled_for, tag)
|
||||
SELECT id, workspace_id, true, now(), tag FROM v2_job WHERE id = ANY($1)",
|
||||
)
|
||||
.bind([Uuid::parse_str(OIDC_JOB)?, Uuid::parse_str(OPEN_JOB)?])
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
set_jwt_secret().await;
|
||||
// The issuer of the minted token.
|
||||
windmill_common::BASE_URL.store(std::sync::Arc::new("http://localhost".to_string()));
|
||||
let base = format!(
|
||||
"http://localhost:{}/api/w/test-workspace",
|
||||
server.addr.port()
|
||||
);
|
||||
let client = reqwest::Client::new();
|
||||
for (job, expected) in [(OIDC_JOB, json!(["oidc:write"])), (OPEN_JOB, json!(null))] {
|
||||
let resp = client
|
||||
.post(format!("{base}/oidc/token/test-audience"))
|
||||
.bearer_auth(job_token(&db, job).await?)
|
||||
.send()
|
||||
.await?;
|
||||
let status = resp.status();
|
||||
let id_token = resp.text().await?;
|
||||
assert_eq!(status, StatusCode::OK, "{id_token}");
|
||||
let payload = id_token.split('.').nth(1).expect("a JWT payload");
|
||||
let claims: serde_json::Value = serde_json::from_slice(
|
||||
&base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(payload)?,
|
||||
)?;
|
||||
assert_eq!(claims["job_token_scopes"], expected, "{job}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -564,6 +564,56 @@ async fn test_single_job_read_authorization(db: Pool<Postgres>) -> anyhow::Resul
|
||||
"run-scoped token must not enumerate jobs (got {status}): {body}"
|
||||
);
|
||||
|
||||
// ---- PATH-SCOPED READ TOKEN: READ_SCOPED_TOKEN is test-user-2's
|
||||
// `jobs:read:f/shared/flow1`. It reads that flow's runs and the steps beneath
|
||||
// them, and nothing else its owner created.
|
||||
for (path, expected) in [
|
||||
(
|
||||
format!("completed/get_result/{FLOW_JOB}"),
|
||||
r#""flow": "done""#,
|
||||
),
|
||||
(
|
||||
format!("completed/get_result/{STEP_JOB}"),
|
||||
"STEP_RESULT_INHERITED",
|
||||
),
|
||||
] {
|
||||
let (status, body) = get(&base, &path, Some("READ_SCOPED_TOKEN")).await;
|
||||
assert!(
|
||||
status.is_success() && body.contains(expected),
|
||||
"read-scoped token must read a run of its flow ({path}, got {status}): {body}"
|
||||
);
|
||||
}
|
||||
for path in [
|
||||
format!("completed/get_result/{VICTIM}"),
|
||||
format!("get/{VICTIM}"),
|
||||
format!("flow/approval_info/{VICTIM}"),
|
||||
] {
|
||||
let (status, body) = get(&base, &path, Some("READ_SCOPED_TOKEN")).await;
|
||||
assert_eq!(
|
||||
status,
|
||||
reqwest::StatusCode::NOT_FOUND,
|
||||
"read-scoped token must not read a job outside its paths ({path}, got {status}): {body}"
|
||||
);
|
||||
}
|
||||
// The lists answer with the top-level runs at its paths only, where the same user's
|
||||
// unscoped token sees the other runs too.
|
||||
for list in ["list", "completed/list"] {
|
||||
let (status, body) = get(&authed_base, list, Some("READ_SCOPED_TOKEN")).await;
|
||||
assert!(status.is_success(), "{list} (got {status}): {body}");
|
||||
let jobs: Vec<serde_json::Value> = serde_json::from_str(&body)?;
|
||||
let mut ids: Vec<&str> = jobs.iter().filter_map(|j| j["id"].as_str()).collect();
|
||||
ids.sort();
|
||||
assert_eq!(ids, [WRAPPED_FLOW_JOB, FLOW_JOB], "{list}: {body}");
|
||||
}
|
||||
let (_, body) = get(&authed_base, "list", Some("SECRET_TOKEN_2")).await;
|
||||
assert!(body.contains(VICTIM), "unscoped list: {body}");
|
||||
let (status, body) = get(&authed_base, "completed/count", Some("READ_SCOPED_TOKEN")).await;
|
||||
assert_eq!(
|
||||
status,
|
||||
reqwest::StatusCode::FORBIDDEN,
|
||||
"read-scoped token must not count the workspace's jobs (got {status}): {body}"
|
||||
);
|
||||
|
||||
// ---- APP EMBED TOKEN: cancellation confined to the app's own jobs. The token
|
||||
// may cancel a job it launched (created_by == viewer), but `cancel_job_api`
|
||||
// denies (NotFound) a job created by someone else, even one the (admin)
|
||||
|
||||
@@ -957,6 +957,7 @@ async fn test_job_labels_propagated_at_push_time(db: Pool<Postgres>) -> anyhow::
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
labels: Some(vec!["prod".to_string(), "deploy".to_string()]),
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.push(&db)
|
||||
.await;
|
||||
@@ -1060,6 +1061,7 @@ async fn test_job_label_filter(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
labels: Some(vec!["prod".to_string()]),
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.push(&db)
|
||||
.await;
|
||||
@@ -1077,6 +1079,7 @@ async fn test_job_label_filter(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
labels: Some(vec!["staging".to_string()]),
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.push(&db)
|
||||
.await;
|
||||
|
||||
@@ -191,6 +191,7 @@ async fn push_job(db: &Pool<Postgres>, content: &str, args: &serde_json::Value)
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("push must succeed");
|
||||
|
||||
@@ -76,6 +76,7 @@ async fn push_preview_and_get_row(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("push must succeed");
|
||||
|
||||
@@ -496,6 +496,28 @@ async def main(x: int):
|
||||
);
|
||||
}
|
||||
|
||||
// `hasattr` does not swallow the KeyError this `__getattr__` raises; released
|
||||
// `wmill` clients ship an `S3Object` built this way.
|
||||
#[test]
|
||||
fn test_python_execd_result_with_keyerror_getattr() {
|
||||
let script = r#"
|
||||
class AttrDict(dict):
|
||||
def __getattr__(self, attr):
|
||||
return self[attr]
|
||||
|
||||
def main(x: str):
|
||||
return AttrDict(s3=x)
|
||||
"#;
|
||||
let results = run_py_raw_protocol_test(
|
||||
&[("f/test/attr_dict", script)],
|
||||
vec![ProtocolCmd::Execd { args: serde_json::json!({"x": "a/b.txt"}) }],
|
||||
);
|
||||
assert_eq!(
|
||||
results,
|
||||
vec![DedicatedWorkerResult::Success(serde_json::json!({"s3": "a/b.txt"}))]
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== Argument Transformation Tests ====================
|
||||
|
||||
#[test]
|
||||
@@ -1278,6 +1300,7 @@ async fn test_python_wac_v2_with_preprocessor(db: Pool<Postgres>) -> anyhow::Res
|
||||
debouncing_settings:
|
||||
windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.arg("who", json!("alice"))
|
||||
.arg("count", json!(7))
|
||||
|
||||
@@ -41,6 +41,7 @@ async fn test_api_restart_at_step_nested_happy(db: Pool<Postgres>) -> anyhow::Re
|
||||
apply_preprocessor: true,
|
||||
version: 1443253234253454,
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.run_until_complete(&db, false, port)
|
||||
.await;
|
||||
@@ -85,6 +86,7 @@ async fn test_api_restart_at_step_top_level_happy(db: Pool<Postgres>) -> anyhow:
|
||||
apply_preprocessor: true,
|
||||
version: 1443253234253454,
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.run_until_complete(&db, false, port)
|
||||
.await;
|
||||
@@ -118,6 +120,7 @@ async fn test_api_restart_at_step_rejects_unknown_step(db: Pool<Postgres>) -> an
|
||||
apply_preprocessor: true,
|
||||
version: 1443253234253454,
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.run_until_complete(&db, false, port)
|
||||
.await;
|
||||
@@ -161,6 +164,7 @@ async fn test_api_restart_at_step_rejects_out_of_range_iteration(
|
||||
apply_preprocessor: true,
|
||||
version: 1443253234253454,
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.run_until_complete(&db, false, port)
|
||||
.await;
|
||||
|
||||
@@ -53,6 +53,7 @@ async fn push_restart(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
@@ -181,6 +181,7 @@ export async function main(path: string, email: string, job_id: string, is_flow:
|
||||
concurrency_settings: ConcurrencySettings::default(),
|
||||
debouncing_settings: DebouncingSettings::default(),
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.run_until_complete(&db, false, server.addr.port())
|
||||
.await;
|
||||
|
||||
@@ -205,6 +205,75 @@ async fn test_delete_jobs_removes_a_conversation_once_its_last_message_goes(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The agent step completes, and so expires, before the flow run that holds the question: its
|
||||
/// job going must leave the answer in place, and the run going must take the whole turn.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_delete_jobs_removes_a_turn_with_its_flow_run(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
let flow_job = Uuid::new_v4();
|
||||
let step_job = Uuid::new_v4();
|
||||
insert_job(&db, WS, flow_job).await?;
|
||||
insert_job(&db, WS, step_job).await?;
|
||||
|
||||
let conv_id = Uuid::new_v4();
|
||||
sqlx::query(
|
||||
"INSERT INTO flow_conversation (id, workspace_id, flow_path, created_by)
|
||||
VALUES ($1, $2, 'f/flow', 'test-user')",
|
||||
)
|
||||
.bind(conv_id)
|
||||
.bind(WS)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
let mut tx = db.begin().await?;
|
||||
for (job_id, message_type) in [
|
||||
(
|
||||
flow_job,
|
||||
windmill_common::flow_conversations::MessageType::User,
|
||||
),
|
||||
(
|
||||
step_job,
|
||||
windmill_common::flow_conversations::MessageType::Assistant,
|
||||
),
|
||||
] {
|
||||
windmill_common::flow_conversations::add_message_to_conversation_tx(
|
||||
&mut tx,
|
||||
conv_id,
|
||||
Some(job_id),
|
||||
flow_job,
|
||||
"hi",
|
||||
message_type,
|
||||
None,
|
||||
true,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
let messages = "SELECT count(*) FROM flow_conversation_message WHERE conversation_id = $1";
|
||||
let mut conn = db.acquire().await?;
|
||||
windmill_common::jobs::delete_jobs(&mut conn, &[step_job]).await?;
|
||||
drop(conn);
|
||||
assert_eq!(
|
||||
count(&db, messages, conv_id).await?,
|
||||
2,
|
||||
"the step's job going must keep the turn"
|
||||
);
|
||||
|
||||
let mut conn = db.acquire().await?;
|
||||
windmill_common::jobs::delete_jobs(&mut conn, &[flow_job]).await?;
|
||||
drop(conn);
|
||||
assert_eq!(
|
||||
count(&db, messages, conv_id).await?,
|
||||
0,
|
||||
"the run going must take the turn"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Turns that start while retention is collecting their conversation must land, not fail:
|
||||
/// the conversation lookup locks the row, so each turn waits for the collector's commit,
|
||||
/// finds the conversation gone, and creates it again — the first insert wins and the other
|
||||
@@ -265,6 +334,7 @@ async fn test_new_turns_wait_for_conversation_cleanup_and_recreate(
|
||||
&mut tx,
|
||||
conv_id,
|
||||
Some(new_job),
|
||||
new_job,
|
||||
"hi again",
|
||||
windmill_common::flow_conversations::MessageType::User,
|
||||
None,
|
||||
|
||||
@@ -212,6 +212,7 @@ async fn test_deno_flow(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
apply_preprocessor: None,
|
||||
pass_flow_input_directly: None,
|
||||
debouncing: None,
|
||||
job_token_scopes: None,
|
||||
},
|
||||
FlowModule {
|
||||
id: "b".to_string(),
|
||||
@@ -260,6 +261,7 @@ async fn test_deno_flow(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
apply_preprocessor: None,
|
||||
pass_flow_input_directly: None,
|
||||
debouncing: None,
|
||||
job_token_scopes: None,
|
||||
}],
|
||||
modules_node: None,
|
||||
}
|
||||
@@ -282,6 +284,7 @@ async fn test_deno_flow(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
apply_preprocessor: None,
|
||||
pass_flow_input_directly: None,
|
||||
debouncing: None,
|
||||
job_token_scopes: None,
|
||||
},
|
||||
],
|
||||
same_worker: false,
|
||||
@@ -398,6 +401,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
apply_preprocessor: None,
|
||||
pass_flow_input_directly: None,
|
||||
debouncing: None,
|
||||
job_token_scopes: None,
|
||||
},
|
||||
FlowModule {
|
||||
id: "b".to_string(),
|
||||
@@ -455,6 +459,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
apply_preprocessor: None,
|
||||
pass_flow_input_directly: None,
|
||||
debouncing: None,
|
||||
job_token_scopes: None,
|
||||
},
|
||||
FlowModule {
|
||||
id: "e".to_string(),
|
||||
@@ -498,6 +503,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
apply_preprocessor: None,
|
||||
pass_flow_input_directly: None,
|
||||
debouncing: None,
|
||||
job_token_scopes: None,
|
||||
},
|
||||
],
|
||||
modules_node: None,
|
||||
@@ -520,6 +526,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
apply_preprocessor: None,
|
||||
pass_flow_input_directly: None,
|
||||
debouncing: None,
|
||||
job_token_scopes: None,
|
||||
},
|
||||
FlowModule {
|
||||
id: "c".to_string(),
|
||||
@@ -569,6 +576,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
apply_preprocessor: None,
|
||||
pass_flow_input_directly: None,
|
||||
debouncing: None,
|
||||
job_token_scopes: None,
|
||||
},
|
||||
],
|
||||
same_worker: true,
|
||||
@@ -1723,6 +1731,286 @@ async fn test_postgresql_cached_connection_resets_session(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The idle cached connection must not hold the server slot a job's own
|
||||
/// connection needs. A role limited to one connection stands in for a
|
||||
/// session-mode pooler with one slot, where the second job would wait forever
|
||||
/// instead of failing.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
#[serial(pg_cache)]
|
||||
async fn test_postgresql_cached_connection_released_for_other_key(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
use windmill_worker::pg_executor::clear_pg_cache;
|
||||
|
||||
initialize_tracing().await;
|
||||
clear_pg_cache().await;
|
||||
|
||||
sqlx::query(
|
||||
"DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'wm_pg_cache_one_conn') THEN
|
||||
CREATE ROLE wm_pg_cache_one_conn LOGIN PASSWORD 'changeme' CONNECTION LIMIT 1;
|
||||
END IF;
|
||||
END $$",
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
// Keys for the same role: one pool scope when only the sslmode differs, two
|
||||
// when the database does, which the role's limit still counts together.
|
||||
let run = |dbname: &str, sslmode: &str| {
|
||||
RunJob::from(JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content: "SELECT 1 as n;".into(),
|
||||
path: None,
|
||||
lock: None,
|
||||
language: ScriptLang::Postgresql,
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
)
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
modules: None,
|
||||
tag: None,
|
||||
}))
|
||||
.arg(
|
||||
"database",
|
||||
json!({"host": "localhost", "port": 5432, "dbname": dbname,
|
||||
"user": "wm_pg_cache_one_conn", "password": "changeme", "sslmode": sslmode}),
|
||||
)
|
||||
.run_until_complete(&db, false, port)
|
||||
};
|
||||
|
||||
// The evicted connection's backend exits asynchronously, so a fresh
|
||||
// connection can briefly still count it. Retrying absorbs that; without the
|
||||
// eviction the cached connection stays open for 60s and every retry fails.
|
||||
for (dbname, sslmode) in [
|
||||
("windmill", "disable"),
|
||||
("windmill", "prefer"),
|
||||
("postgres", "disable"),
|
||||
("windmill", "disable"),
|
||||
] {
|
||||
let mut result = json!(null);
|
||||
for _ in 0..5 {
|
||||
result = run(dbname, sslmode).await.json_result().unwrap();
|
||||
if result == json!([{"n": 1}]) {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
}
|
||||
assert_eq!(result, json!([{"n": 1}]), "{dbname} sslmode={sslmode}");
|
||||
}
|
||||
|
||||
clear_pg_cache().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A transaction a script leaves open must not carry over into the next job
|
||||
/// that would reuse the connection.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
#[serial(pg_cache)]
|
||||
async fn test_postgresql_open_transaction_not_reused(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
use windmill_worker::pg_executor::clear_pg_cache;
|
||||
|
||||
initialize_tracing().await;
|
||||
clear_pg_cache().await;
|
||||
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
let dbname: String = sqlx::query_scalar("SELECT current_database()")
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
|
||||
let run = |content: &str| {
|
||||
RunJob::from(JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content: content.to_string(),
|
||||
path: None,
|
||||
lock: None,
|
||||
language: ScriptLang::Postgresql,
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
)
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
modules: None,
|
||||
tag: None,
|
||||
}))
|
||||
.arg(
|
||||
"database",
|
||||
json!({"host": "localhost", "port": 5432, "dbname": dbname, "user": "postgres", "password": "changeme"}),
|
||||
)
|
||||
.run_until_complete(&db, false, port)
|
||||
};
|
||||
|
||||
run("SELECT 1 as n;").await.json_result().unwrap();
|
||||
let opened = run("BEGIN; SELECT 1 as n;").await.json_result().unwrap();
|
||||
assert_eq!(opened, json!([{"n": 1}]));
|
||||
run("SELECT 2 as n;").await.json_result().unwrap();
|
||||
|
||||
// A connection kept in the cache with the transaction open would sit idle
|
||||
// in it, holding its locks, and run the next job inside it.
|
||||
let mut idle_in_transaction = -1;
|
||||
for _ in 0..20 {
|
||||
idle_in_transaction = sqlx::query_scalar::<_, i64>(
|
||||
"SELECT count(*) FROM pg_stat_activity
|
||||
WHERE datname = current_database() AND state LIKE 'idle in transaction%'",
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
if idle_in_transaction == 0 {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
assert_eq!(idle_in_transaction, 0);
|
||||
|
||||
clear_pg_cache().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Closing the connection of a cancelled job does not stop its query: the server
|
||||
/// only notices once it next writes to the client.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
#[serial(pg_cache)]
|
||||
async fn test_postgresql_cancelled_job_stops_its_query(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
use windmill_worker::pg_executor::clear_pg_cache;
|
||||
|
||||
initialize_tracing().await;
|
||||
clear_pg_cache().await;
|
||||
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
let dbname: String = sqlx::query_scalar("SELECT current_database()")
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
let still_running = "SELECT count(*) FROM pg_stat_activity
|
||||
WHERE datname = current_database() AND state = 'active'
|
||||
AND pid <> pg_backend_pid() AND query LIKE '%pg_sleep(30)%'";
|
||||
|
||||
let cancelled = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let job = RunJob::from(JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content: "SELECT pg_sleep(30);".to_string(),
|
||||
path: None,
|
||||
lock: None,
|
||||
language: ScriptLang::Postgresql,
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
modules: None,
|
||||
tag: None,
|
||||
}))
|
||||
.arg(
|
||||
"database",
|
||||
json!({"host": "localhost", "port": 5432, "dbname": dbname, "user": "postgres", "password": "changeme"}),
|
||||
)
|
||||
.run_until_complete_with(&db, false, port, |id| {
|
||||
let (db, cancelled) = (db.clone(), cancelled.clone());
|
||||
async move {
|
||||
tokio::spawn(async move {
|
||||
while sqlx::query_scalar::<_, i64>(still_running)
|
||||
.fetch_one(&db)
|
||||
.await
|
||||
.unwrap()
|
||||
== 0
|
||||
{
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
}
|
||||
sqlx::query("UPDATE v2_job_queue SET canceled_by = 'test-user' WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&db)
|
||||
.await
|
||||
.unwrap();
|
||||
cancelled.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
});
|
||||
}
|
||||
})
|
||||
.await;
|
||||
assert!(!job.success);
|
||||
// Otherwise the job failed before its query ran, and there was nothing to stop.
|
||||
assert!(cancelled.load(std::sync::atomic::Ordering::Relaxed));
|
||||
|
||||
let mut running = -1;
|
||||
for _ in 0..50 {
|
||||
running = sqlx::query_scalar::<_, i64>(still_running)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
if running == 0 {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
assert_eq!(running, 0);
|
||||
|
||||
clear_pg_cache().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A worker alternating between two databases keeps a connection for each.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
#[serial(pg_cache)]
|
||||
async fn test_postgresql_cache_keeps_a_connection_per_database(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
use std::sync::atomic::Ordering;
|
||||
use windmill_worker::pg_executor::{clear_pg_cache, CACHE_HITS};
|
||||
|
||||
initialize_tracing().await;
|
||||
clear_pg_cache().await;
|
||||
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let run = |dbname: &str| {
|
||||
RunJob::from(JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content: "SELECT current_database() as d;".into(),
|
||||
path: None,
|
||||
lock: None,
|
||||
language: ScriptLang::Postgresql,
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
|
||||
)
|
||||
.into(),
|
||||
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
|
||||
modules: None,
|
||||
tag: None,
|
||||
}))
|
||||
.arg(
|
||||
"database",
|
||||
json!({"host": "localhost", "port": 5432, "dbname": dbname,
|
||||
"user": "postgres", "password": "changeme"}),
|
||||
)
|
||||
.run_until_complete(&db, false, port)
|
||||
};
|
||||
|
||||
let hits_before = CACHE_HITS.load(Ordering::Relaxed);
|
||||
for dbname in ["windmill", "postgres", "windmill", "postgres"] {
|
||||
let result = run(dbname).await.json_result().unwrap();
|
||||
assert_eq!(result, json!([{"d": dbname}]));
|
||||
}
|
||||
let hits = CACHE_HITS.load(Ordering::Relaxed) - hits_before;
|
||||
assert_eq!(
|
||||
hits, 2,
|
||||
"the last two jobs should reuse a cached connection"
|
||||
);
|
||||
|
||||
clear_pg_cache().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs multiple PG jobs through a SINGLE worker (like production) to verify
|
||||
/// that SET ROLE / search_path changes do not leak across jobs.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
@@ -5653,6 +5941,7 @@ async fn test_flow_tag_judged_as_written_before_preprocessor(
|
||||
apply_preprocessor: true,
|
||||
version: 1443253234253456,
|
||||
labels: None,
|
||||
job_token_scopes: None,
|
||||
})
|
||||
.as_user("test-user-2", "test2@windmill.dev")
|
||||
.run_until_complete(&db, false, port)
|
||||
|
||||
@@ -39,6 +39,7 @@ lazy_static::lazy_static! {
|
||||
pub const OPENAI_BASE_URL: &str = "https://api.openai.com/v1";
|
||||
pub const DEEPSEEK_BASE_URL: &str = "https://api.deepseek.com/v1";
|
||||
pub const GOOGLE_AI_BASE_URL: &str = "https://generativelanguage.googleapis.com/v1beta";
|
||||
pub const TYPESAFE_BASE_URL: &str = "https://api.typesafe.ai/v1";
|
||||
|
||||
/// Hosts that serve the OpenAI API with Azure conventions: Azure OpenAI
|
||||
/// (`*.openai.azure.com`), AI Foundry (`*.services.ai.azure.com`,
|
||||
@@ -122,9 +123,35 @@ pub enum AIProvider {
|
||||
CustomAI,
|
||||
#[serde(rename = "aws_bedrock")]
|
||||
AWSBedrock,
|
||||
/// TypeSafe's Jev decision model. It answers typed questions rather than chatting, so only
|
||||
/// an AI decision runs it (`run_systemone`), never the agent loop.
|
||||
TypeSafe,
|
||||
/// Cloudflare Workers AI, for its Jev-compatible decision models (Clef). Decisions only, as
|
||||
/// for TypeSafe; its base URL comes from the resource's account id.
|
||||
Cloudflare,
|
||||
}
|
||||
|
||||
/// The Workers AI models URL of a Cloudflare account. The id goes into the path, so it is held to
|
||||
/// the characters an account id has rather than trusted to stay inside its segment.
|
||||
pub fn cloudflare_workers_ai_base_url(account_id: Option<&str>) -> Result<String> {
|
||||
match account_id {
|
||||
Some(id) if !id.is_empty() && id.chars().all(|c| c.is_ascii_alphanumeric()) => Ok(format!(
|
||||
"https://api.cloudflare.com/client/v4/accounts/{id}/ai/run/@cf/cloudflare"
|
||||
)),
|
||||
Some(_) => Err(Error::BadRequest(
|
||||
"The Cloudflare account_id must be letters and digits only".to_string(),
|
||||
)),
|
||||
None => Err(Error::BadRequest(
|
||||
"A Cloudflare resource needs an account_id".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
impl AIProvider {
|
||||
/// A provider that answers decisions (typed questions) and serves no chat.
|
||||
pub fn is_decision_provider(&self) -> bool {
|
||||
matches!(self, AIProvider::TypeSafe | AIProvider::Cloudflare)
|
||||
}
|
||||
|
||||
/// Get the base URL for the AI provider
|
||||
pub async fn get_base_url(&self, resource_base_url: Option<String>, db: &DB) -> Result<String> {
|
||||
if let Some(base_url) = resource_base_url {
|
||||
@@ -177,6 +204,10 @@ impl AIProvider {
|
||||
AIProvider::TogetherAI => Ok("https://api.together.xyz/v1".to_string()),
|
||||
AIProvider::Anthropic => Ok("https://api.anthropic.com/v1".to_string()),
|
||||
AIProvider::Mistral => Ok("https://api.mistral.ai/v1".to_string()),
|
||||
AIProvider::TypeSafe => Ok(TYPESAFE_BASE_URL.to_string()),
|
||||
AIProvider::Cloudflare => Err(Error::BadRequest(
|
||||
"A Cloudflare resource needs an account_id".to_string(),
|
||||
)),
|
||||
p @ (AIProvider::CustomAI | AIProvider::AzureOpenAI | AIProvider::AzureFoundry) => {
|
||||
Err(Error::BadRequest(format!(
|
||||
"{:?} provider requires a base URL in the resource",
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
//! AI decision steps: one call to a System One endpoint (TypeSafe's Jev, or Cloudflare's
|
||||
//! Jev-compatible Clef on Workers AI), which answers typed questions (`choice`, `score`, `noul`)
|
||||
//! about a state with calibrated probabilities instead of text.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{value::RawValue, Map, Value};
|
||||
use windmill_common::error::{Error, Result};
|
||||
|
||||
use crate::{
|
||||
ai_providers::AIProvider,
|
||||
credentials::ProviderCredentials,
|
||||
proxy::{common_outbound_headers, retain_effective_credentials},
|
||||
types::{ProviderWithResource, TokenUsage},
|
||||
utils::pinned_ai_client_for,
|
||||
};
|
||||
|
||||
/// The inputs of an AI decision step or tool, as its `input_transforms` resolve them.
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct AIDecisionArgs {
|
||||
pub provider: ProviderWithResource,
|
||||
#[serde(default)]
|
||||
pub state: Option<Value>,
|
||||
#[serde(default)]
|
||||
pub questions: Option<Value>,
|
||||
}
|
||||
|
||||
/// The alias TypeSafe keeps on its current model, sent when the step names none.
|
||||
pub const TYPESAFE_DEFAULT_MODEL: &str = "jev-latest";
|
||||
/// Cloudflare's larger decision model, run when the step names none.
|
||||
pub const CLOUDFLARE_DEFAULT_MODEL: &str = "clef";
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SystemOneRequest<'a> {
|
||||
model: &'a str,
|
||||
state: &'a Value,
|
||||
questions: &'a Map<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SystemOneResponse {
|
||||
#[serde(default)]
|
||||
model: Option<String>,
|
||||
answers: Box<RawValue>,
|
||||
#[serde(default)]
|
||||
usage: Option<SystemOneUsage>,
|
||||
}
|
||||
|
||||
/// Workers AI wraps the System One response in its API envelope.
|
||||
#[derive(Deserialize)]
|
||||
struct CloudflareEnvelope {
|
||||
result: SystemOneResponse,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SystemOneUsage {
|
||||
input_tokens: Option<i32>,
|
||||
output_tokens: Option<i32>,
|
||||
}
|
||||
|
||||
/// A decision's result. `output` holds the answers keyed by question name, where an agent's
|
||||
/// holds its answer, so a later step reads both as `results.<step>.output`.
|
||||
#[derive(Serialize)]
|
||||
pub struct DecisionResult {
|
||||
pub output: Box<RawValue>,
|
||||
/// The version that answered, which `jev-latest` resolves to.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub usage: Option<TokenUsage>,
|
||||
}
|
||||
|
||||
/// The state and questions of a decision, refused here rather than by the endpoint when
|
||||
/// either is missing or of a shape TypeSafe rejects: a flow that passes an empty expression result
|
||||
/// should read what it forgot, without a request going out.
|
||||
pub fn decision_inputs<'a>(
|
||||
state: Option<&'a Value>,
|
||||
questions: Option<&'a Value>,
|
||||
) -> Result<(&'a Value, &'a Map<String, Value>)> {
|
||||
let state = match state {
|
||||
None | Some(Value::Null) => {
|
||||
return Err(Error::BadRequest(
|
||||
"'state' must be provided for an AI decision".to_string(),
|
||||
))
|
||||
}
|
||||
Some(Value::String(s)) if s.trim().is_empty() => {
|
||||
return Err(Error::BadRequest(
|
||||
"'state' must be provided for an AI decision".to_string(),
|
||||
))
|
||||
}
|
||||
Some(state @ (Value::String(_) | Value::Object(_) | Value::Array(_))) => state,
|
||||
Some(_) => {
|
||||
return Err(Error::BadRequest(
|
||||
"'state' must be a text, an object or an array for an AI decision".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
let questions = match questions {
|
||||
Some(Value::Object(questions)) if !questions.is_empty() => questions,
|
||||
_ => {
|
||||
return Err(Error::BadRequest(
|
||||
"'questions' must be an object naming at least one question for an AI decision"
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
Ok((state, questions))
|
||||
}
|
||||
|
||||
pub async fn run_systemone(
|
||||
credentials: &ProviderCredentials,
|
||||
model: &str,
|
||||
state: &Value,
|
||||
questions: &Map<String, Value>,
|
||||
timeout: Duration,
|
||||
) -> Result<DecisionResult> {
|
||||
let cloudflare = credentials.provider == AIProvider::Cloudflare;
|
||||
let model = match model.trim() {
|
||||
"" if cloudflare => CLOUDFLARE_DEFAULT_MODEL,
|
||||
"" => TYPESAFE_DEFAULT_MODEL,
|
||||
model => model,
|
||||
};
|
||||
let base = credentials.base_url.trim_end_matches('/');
|
||||
// Workers AI serves each model at its own URL; the model goes into the path, so it is held to
|
||||
// the characters a model id has.
|
||||
let endpoint = if cloudflare {
|
||||
if !model
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))
|
||||
{
|
||||
return Err(Error::BadRequest(format!(
|
||||
"Invalid Cloudflare decision model: {model}"
|
||||
)));
|
||||
}
|
||||
format!("{base}/{model}")
|
||||
} else {
|
||||
format!("{base}/systemone")
|
||||
};
|
||||
let vendor = if cloudflare { "Cloudflare" } else { "TypeSafe" };
|
||||
let auth_headers = retain_effective_credentials(
|
||||
credentials,
|
||||
vec![(
|
||||
"Authorization",
|
||||
format!(
|
||||
"Bearer {}",
|
||||
credentials.api_key.as_deref().unwrap_or_default()
|
||||
),
|
||||
)],
|
||||
);
|
||||
|
||||
// The endpoint derives from the resource's base URL, so the connect is pinned to the
|
||||
// address the SSRF check validated.
|
||||
let client = pinned_ai_client_for(&credentials.base_url).await?;
|
||||
let mut request = client.post(&endpoint).timeout(timeout);
|
||||
for (name, value) in auth_headers {
|
||||
request = request.header(name, value);
|
||||
}
|
||||
for (name, value) in common_outbound_headers(credentials) {
|
||||
request = request.header(name, value);
|
||||
}
|
||||
|
||||
let response = request
|
||||
.json(&SystemOneRequest { model, state, questions })
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| Error::ExecutionErr(format!("Failed to call {vendor}: {e}")))?;
|
||||
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
let body = response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| "<failed to read body>".to_string());
|
||||
return Err(Error::ExecutionErr(format!(
|
||||
"{vendor} error calling {endpoint}: {status} - {body}"
|
||||
)));
|
||||
}
|
||||
|
||||
let body = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| Error::ExecutionErr(format!("Failed to read the {vendor} response: {e}")))?;
|
||||
// Tried in turn rather than as an untagged enum, which cannot hold the raw `answers`.
|
||||
let parsed = match serde_json::from_str::<SystemOneResponse>(&body) {
|
||||
Ok(parsed) => parsed,
|
||||
Err(e) => match serde_json::from_str::<CloudflareEnvelope>(&body) {
|
||||
Ok(envelope) => envelope.result,
|
||||
Err(_) => {
|
||||
return Err(Error::ExecutionErr(format!(
|
||||
"Unexpected {vendor} response ({e}): {body}"
|
||||
)))
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Ok(DecisionResult {
|
||||
output: parsed.answers,
|
||||
model: parsed.model,
|
||||
// Dropped when empty, as an agent's is, so `usage` is either counted or absent.
|
||||
usage: parsed
|
||||
.usage
|
||||
.map(|u| TokenUsage::from_input_output(u.input_tokens, u.output_tokens))
|
||||
.filter(|u| !u.is_empty()),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
/// The account id goes into the URL path, so anything that could leave its segment is refused.
|
||||
#[test]
|
||||
fn cloudflare_account_id_stays_in_its_segment() {
|
||||
use crate::ai_providers::cloudflare_workers_ai_base_url;
|
||||
assert_eq!(
|
||||
cloudflare_workers_ai_base_url(Some("abc123")).unwrap(),
|
||||
"https://api.cloudflare.com/client/v4/accounts/abc123/ai/run/@cf/cloudflare"
|
||||
);
|
||||
for id in [None, Some("a/b"), Some("../x"), Some("a?b"), Some("")] {
|
||||
assert!(cloudflare_workers_ai_base_url(id).is_err(), "{id:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Refused are the values a flow produces when it forgot the input (no key, `null`, a blank
|
||||
/// string from an empty expression) and the scalars TypeSafe rejects. A text, an object or an
|
||||
/// array is a state, whatever it holds.
|
||||
#[test]
|
||||
fn decision_inputs_refuse_what_typesafe_would() {
|
||||
let questions = json!({"urgent": {"type": "noul", "instructions": "Is it urgent?"}});
|
||||
for state in [
|
||||
None,
|
||||
Some(json!(null)),
|
||||
Some(json!(" ")),
|
||||
Some(json!(false)),
|
||||
Some(json!(0)),
|
||||
] {
|
||||
let err = decision_inputs(state.as_ref(), Some(&questions)).unwrap_err();
|
||||
assert!(err.to_string().contains("'state'"), "{err}");
|
||||
}
|
||||
for state in [json!("0"), json!({}), json!([]), json!({"m": "hi"})] {
|
||||
assert!(
|
||||
decision_inputs(Some(&state), Some(&questions)).is_ok(),
|
||||
"{state}"
|
||||
);
|
||||
}
|
||||
for questions in [
|
||||
None,
|
||||
Some(json!({})),
|
||||
Some(json!([])),
|
||||
Some(json!("urgent")),
|
||||
] {
|
||||
let err = decision_inputs(Some(&json!("hi")), questions.as_ref()).unwrap_err();
|
||||
assert!(err.to_string().contains("'questions'"), "{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,9 @@ pub async fn upload_image_to_s3(
|
||||
})
|
||||
}
|
||||
|
||||
// Bounds each attachment's download before base64 expansion; provider limits may be lower.
|
||||
const MAX_S3_MEDIA_BYTES: usize = 30 * 1024 * 1024;
|
||||
|
||||
/// Download an S3 image and convert it to a base64 data URL.
|
||||
///
|
||||
/// The caller must provide an AuthedClient authorized for `workspace_id`.
|
||||
@@ -57,7 +60,12 @@ pub async fn download_and_encode_s3_image(
|
||||
) -> Result<(String, String), Error> {
|
||||
// Download the image from S3
|
||||
let image_bytes = client
|
||||
.download_s3_file(workspace_id, &image.s3, image.storage.clone())
|
||||
.download_s3_file(
|
||||
workspace_id,
|
||||
&image.s3,
|
||||
image.storage.clone(),
|
||||
Some(MAX_S3_MEDIA_BYTES),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| Error::internal_err(format!("Failed to download S3 image: {}", e)))?;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ pub mod ai_google;
|
||||
pub mod ai_providers;
|
||||
pub mod ai_types;
|
||||
pub mod credentials;
|
||||
pub mod decision;
|
||||
pub mod image_handler;
|
||||
pub mod model_context;
|
||||
pub mod providers;
|
||||
|
||||
@@ -204,6 +204,12 @@ pub fn create_query_builder(
|
||||
AIProvider::AzureFoundry if AIProvider::is_anthropic_model(model) => Box::new(
|
||||
AnthropicQueryBuilder::new(AIProvider::AzureFoundry, AIPlatform::Standard),
|
||||
),
|
||||
// Only for forwarding through the API proxy, which is bearer auth on `<base>/<path>` as
|
||||
// TypeSafe and Cloudflare expect. Neither serves a chat route for its decision models, and
|
||||
// `run_agent` refuses both before a chat request could be built.
|
||||
AIProvider::TypeSafe | AIProvider::Cloudflare => {
|
||||
Box::new(OtherQueryBuilder::new(credentials.provider.clone()))
|
||||
}
|
||||
_ => Box::new(OtherQueryBuilder::new(credentials.provider.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,9 @@ pub fn proxy_execution_mode(provider: &AIProvider) -> ProxyExecutionMode {
|
||||
| AIProvider::Groq
|
||||
| AIProvider::OpenRouter
|
||||
| AIProvider::TogetherAI
|
||||
| AIProvider::CustomAI => ProxyExecutionMode::HttpForward,
|
||||
| AIProvider::CustomAI
|
||||
| AIProvider::TypeSafe
|
||||
| AIProvider::Cloudflare => ProxyExecutionMode::HttpForward,
|
||||
AIProvider::GoogleAI => ProxyExecutionMode::NativeGoogleAi,
|
||||
AIProvider::AWSBedrock => ProxyExecutionMode::NativeAwsBedrock,
|
||||
}
|
||||
@@ -444,6 +446,8 @@ mod tests {
|
||||
(AIProvider::OpenRouter, ProxyExecutionMode::HttpForward),
|
||||
(AIProvider::TogetherAI, ProxyExecutionMode::HttpForward),
|
||||
(AIProvider::CustomAI, ProxyExecutionMode::HttpForward),
|
||||
(AIProvider::TypeSafe, ProxyExecutionMode::HttpForward),
|
||||
(AIProvider::Cloudflare, ProxyExecutionMode::HttpForward),
|
||||
(AIProvider::GoogleAI, ProxyExecutionMode::NativeGoogleAi),
|
||||
(AIProvider::AWSBedrock, ProxyExecutionMode::NativeAwsBedrock),
|
||||
];
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use windmill_common::{client::AuthedClient, error::Error};
|
||||
use windmill_common::{client::AuthedClient, error::Error, utils::strip_json_nul};
|
||||
use windmill_types::s3::S3Object;
|
||||
|
||||
use crate::ai_types::OpenAIToolCall;
|
||||
@@ -92,6 +94,39 @@ pub enum ParsedResponse {
|
||||
},
|
||||
}
|
||||
|
||||
impl ParsedResponse {
|
||||
/// Drops every U+0000 the model emitted. Postgres stores neither a NUL in `text` nor its
|
||||
/// `\u0000` escape in `jsonb`, and this output is written to both: tool job args, the flow
|
||||
/// status, agent memory, conversation rows.
|
||||
pub fn without_nul(mut self) -> Self {
|
||||
fn strip(s: &mut String) {
|
||||
if s.contains('\0') {
|
||||
s.retain(|c| c != '\0');
|
||||
}
|
||||
}
|
||||
if let ParsedResponse::Text { content, reasoning, tool_calls, annotations, .. } = &mut self
|
||||
{
|
||||
for text in [content, reasoning].into_iter().flatten() {
|
||||
strip(text);
|
||||
}
|
||||
for annotation in annotations {
|
||||
strip(&mut annotation.url);
|
||||
annotation.title.as_mut().into_iter().for_each(strip);
|
||||
}
|
||||
for tool_call in tool_calls {
|
||||
strip(&mut tool_call.id);
|
||||
strip(&mut tool_call.function.name);
|
||||
// Arguments are serialized JSON, where a NUL is spelled as its escape.
|
||||
strip(&mut tool_call.function.arguments);
|
||||
if let Cow::Owned(stripped) = strip_json_nul(&tool_call.function.arguments) {
|
||||
tool_call.function.arguments = stripped;
|
||||
}
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for streaming AI events to a sink (e.g., database persistence).
|
||||
/// Implemented by the worker's StreamEventProcessor.
|
||||
#[async_trait]
|
||||
|
||||
@@ -269,6 +269,12 @@ pub struct ProviderResource {
|
||||
/// credentials exchange this crate does not perform.
|
||||
#[serde(default, deserialize_with = "empty_string_as_none")]
|
||||
pub token_url: Option<String>,
|
||||
/// The API token of a `cloudflare` resource, which names it `token` rather than `api_key`.
|
||||
#[serde(default, deserialize_with = "empty_string_as_none")]
|
||||
pub token: Option<String>,
|
||||
/// The account of a `cloudflare` resource, which its Workers AI URL is built from.
|
||||
#[serde(default, deserialize_with = "empty_string_as_none")]
|
||||
pub account_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -301,6 +307,11 @@ impl ProviderWithResource {
|
||||
}
|
||||
|
||||
pub async fn get_base_url(&self, db: &DB) -> Result<String, Error> {
|
||||
if self.kind == AIProvider::Cloudflare && self.resource.base_url.is_none() {
|
||||
return crate::ai_providers::cloudflare_workers_ai_base_url(
|
||||
self.resource.account_id.as_deref(),
|
||||
);
|
||||
}
|
||||
self.kind
|
||||
.get_base_url(self.resource.base_url.clone(), db)
|
||||
.await
|
||||
@@ -320,7 +331,14 @@ impl ProviderWithResource {
|
||||
Ok(ProviderCredentials {
|
||||
provider: self.kind.clone(),
|
||||
base_url,
|
||||
api_key: self.resource.api_key.clone(),
|
||||
api_key: match self.kind {
|
||||
AIProvider::Cloudflare => self
|
||||
.resource
|
||||
.api_key
|
||||
.clone()
|
||||
.or_else(|| self.resource.token.clone()),
|
||||
_ => self.resource.api_key.clone(),
|
||||
},
|
||||
access_token: None,
|
||||
organization_id: None,
|
||||
user: None,
|
||||
|
||||
@@ -104,6 +104,32 @@ pub struct ExpiringAuthCache {
|
||||
pub job_id: Option<uuid::Uuid>,
|
||||
}
|
||||
|
||||
/// Whether `target` belongs to the flow run of the job `job_id`: the job itself, one of its
|
||||
/// ancestors, or a job whose parent or root is one of them (a sibling step, a loop or branch
|
||||
/// iteration, any job pushed as a child of one of them). A lookup that fails reads as no.
|
||||
pub async fn job_in_same_flow_run(db: &DB, job_id: uuid::Uuid, target: uuid::Uuid) -> bool {
|
||||
if job_id == target {
|
||||
return true;
|
||||
}
|
||||
sqlx::query_scalar!(
|
||||
"SELECT EXISTS (
|
||||
SELECT 1 FROM v2_job o, v2_job t,
|
||||
LATERAL (SELECT ARRAY_REMOVE(ARRAY[o.id, o.parent_job, o.root_job,
|
||||
o.flow_innermost_root_job], NULL) AS run) l
|
||||
WHERE o.id = $1 AND t.id = $2 AND t.workspace_id = o.workspace_id
|
||||
AND (t.id = ANY(l.run) OR t.parent_job = ANY(l.run)
|
||||
OR t.root_job = ANY(l.run) OR t.flow_innermost_root_job = ANY(l.run))
|
||||
)",
|
||||
job_id,
|
||||
target
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub struct AuthCache {
|
||||
db: DB,
|
||||
superadmin_secret: Option<String>,
|
||||
@@ -1131,8 +1157,20 @@ pub async fn resolve_opt_job_authed(
|
||||
return Err((err, parts));
|
||||
}
|
||||
}
|
||||
let own_job_runtime_route = match opt_job_authed.job_id {
|
||||
Some(job_id) if opt_job_authed.authed.scopes.is_some() => {
|
||||
crate::scopes::is_own_job_runtime_route(path, method, job_id)
|
||||
|| match crate::scopes::flow_run_read_route_job(path, method) {
|
||||
Some(target) => {
|
||||
job_in_same_flow_run(&cache.db, job_id, target).await
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
let authed = &mut opt_job_authed.authed;
|
||||
if authed.scopes.is_some() {
|
||||
if authed.scopes.is_some() && !own_job_runtime_route {
|
||||
transform_old_scope_to_new_scope(authed.scopes.as_mut());
|
||||
|
||||
if let Err(err) = crate::scopes::check_scopes_for_route(
|
||||
|
||||
@@ -35,6 +35,7 @@ use windmill_common::{
|
||||
};
|
||||
|
||||
use scopes::ScopeDefinition;
|
||||
use windmill_common::scopes::scope_contains;
|
||||
|
||||
// Re-export key auth types and functions
|
||||
pub use auth::{
|
||||
@@ -475,11 +476,11 @@ pub fn is_effectively_unscoped(scopes: Option<&[String]>) -> bool {
|
||||
/// refused. An admin check is not a substitute — it answers for the user behind the
|
||||
/// token, not for the token's own scopes.
|
||||
///
|
||||
/// This bounds the token making the request, not every route to the key. A job token
|
||||
/// is minted unscoped from its owner's privileges, so a `jobs:run` token still reaches
|
||||
/// the key indirectly by running a job as a workspace admin — the same property that
|
||||
/// lets git-sync export it. Confining that means not inheriting unscoped privilege
|
||||
/// into job tokens, which is a far wider change than this guard.
|
||||
/// This bounds the token making the request, not every route to the key. The jobs a
|
||||
/// scoped user token runs get a token with their owner's privileges, capped only by the
|
||||
/// runnable's own `job_token_scopes` (see [`caller_scope_ceiling`]), so a `jobs:run`
|
||||
/// token still reaches the key indirectly by running an unrestricted job as a workspace
|
||||
/// admin — the same property that lets git-sync export it.
|
||||
pub fn forbid_scoped_token_workspace_key(authed: &ApiAuthed) -> error::Result<()> {
|
||||
if is_effectively_unscoped(authed.scopes.as_deref()) {
|
||||
return Ok(());
|
||||
@@ -492,6 +493,50 @@ pub fn forbid_scoped_token_workspace_key(authed: &ApiAuthed) -> error::Result<()
|
||||
))
|
||||
}
|
||||
|
||||
/// For a route that authenticates its token itself rather than through the route layer
|
||||
/// (which checks scopes): a job token restricted by `job_token_scopes` must still hold
|
||||
/// `required` there. Other credentials keep the access those routes always gave them.
|
||||
pub fn check_job_token_scope<F>(authed: &ApiAuthed, required: F) -> error::Result<()>
|
||||
where
|
||||
F: FnOnce() -> String,
|
||||
{
|
||||
if authed.job_id.is_some() {
|
||||
check_scopes(authed, required)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The cap on the token of a job pushed on `authed`'s request, to pass to `push` as its
|
||||
/// scope ceiling. A job token caps every job it starts at its own job's scopes, so a
|
||||
/// restricted job cannot widen its reach by running another one; the job row is the
|
||||
/// source rather than the token's claims, which the MCP proxy narrows per route. Any
|
||||
/// other credential caps nothing: the jobs it runs act as their owner, as they always did.
|
||||
pub async fn caller_scope_ceiling(
|
||||
db: &windmill_common::DB,
|
||||
authed: &ApiAuthed,
|
||||
) -> error::Result<Option<Vec<String>>> {
|
||||
let Some(job_id) = authed.job_id else {
|
||||
return Ok(None);
|
||||
};
|
||||
// A job token is minted with scopes exactly when its job is restricted, so an unscoped one
|
||||
// has nothing to read.
|
||||
if authed.scopes.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
let row = sqlx::query_scalar!(
|
||||
"SELECT job_token_scopes FROM job_perms WHERE job_id = $1",
|
||||
job_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
Ok(match row {
|
||||
Some(scopes) => scopes,
|
||||
// The job left the queue and its row was swept: only the token's own claims are left.
|
||||
None => authed.scopes.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Enforce monotonic privilege when a token lifecycle endpoint mints or rescopes
|
||||
/// a credential on behalf of `authed`: the resulting credential must never be
|
||||
/// more privileged than the caller's own token.
|
||||
@@ -604,83 +649,6 @@ fn first_filter_tags(scopes: Option<&[String]>) -> Option<Vec<&str>> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether `caller` grants at least everything `requested` grants (directional
|
||||
/// containment).
|
||||
///
|
||||
/// This is intentionally NOT `ScopeDefinition::includes`: that method answers
|
||||
/// "does this scope grant access to a required action" using OR semantics over
|
||||
/// resources (any overlap counts, and a `*` on either side matches), which is
|
||||
/// correct for access checks but unsafe for subset checks — it would let a
|
||||
/// token scoped to `scripts:read:f/team/a` mint `scripts:read:*` or
|
||||
/// `scripts:read:f/team/a,f/other/b`. Subset containment instead requires that
|
||||
/// EVERY requested resource is covered by SOME caller resource.
|
||||
fn scope_contains(caller: &ScopeDefinition, requested: &ScopeDefinition) -> bool {
|
||||
if caller.domain != requested.domain {
|
||||
return false;
|
||||
}
|
||||
|
||||
// write subsumes read; otherwise the action must match exactly.
|
||||
match (caller.action.as_str(), requested.action.as_str()) {
|
||||
(c, r) if c == r || (c == "write" && r == "read") => {}
|
||||
// Apps only: `write` covers `run` (see `ScopeDefinition::includes`), so an
|
||||
// app-editor token can mint the narrower run-only credential.
|
||||
("write", "run") if caller.domain == "apps" => {}
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
if caller.domain == "jobs" && caller.action == "run" {
|
||||
match (&caller.kind, &requested.kind) {
|
||||
(Some(caller_kind), Some(requested_kind)) if caller_kind != requested_kind => {
|
||||
return false
|
||||
}
|
||||
// Caller pinned to a kind, but the request covers any kind.
|
||||
(Some(_), None) => return false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
match (&caller.resource, &requested.resource) {
|
||||
// Caller is unrestricted on resources: covers everything.
|
||||
(None, _) => true,
|
||||
// Caller is resource-restricted but the request is not: broader, unless the
|
||||
// caller lists `*` and so already spans every path. Kept in step with
|
||||
// `ScopeDefinition::includes`, which accepts that same grant for a
|
||||
// whole-collection read: what a token may exercise, it may also delegate.
|
||||
(Some(caller_resources), None) => caller_resources.iter().any(|r| r == "*"),
|
||||
(Some(caller_resources), Some(requested_resources)) => {
|
||||
resource_set_contains(caller_resources, requested_resources)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Every resource in `requested` must be covered by some resource in `caller`.
|
||||
fn resource_set_contains(caller: &[String], requested: &[String]) -> bool {
|
||||
if caller.iter().any(|r| r == "*") {
|
||||
return true;
|
||||
}
|
||||
requested
|
||||
.iter()
|
||||
.all(|req| req != "*" && caller.iter().any(|c| resource_covers(c, req)))
|
||||
}
|
||||
|
||||
/// Directional: does the single caller resource pattern cover `requested`?
|
||||
/// `caller` may be an exact path or a `<prefix>/*` subtree wildcard; `requested`
|
||||
/// may itself be a subtree wildcard, in which case the whole requested subtree
|
||||
/// must fall within the caller's subtree.
|
||||
fn resource_covers(caller: &str, requested: &str) -> bool {
|
||||
if caller == requested {
|
||||
return true;
|
||||
}
|
||||
let Some(prefix) = caller.strip_suffix("/*") else {
|
||||
// An exact caller resource only covers itself (handled above).
|
||||
return false;
|
||||
};
|
||||
let requested_base = requested.strip_suffix("/*").unwrap_or(requested);
|
||||
requested_base == prefix
|
||||
|| (requested_base.starts_with(prefix)
|
||||
&& requested_base.as_bytes().get(prefix.len()) == Some(&b'/'))
|
||||
}
|
||||
|
||||
/// Returns a predicate that checks whether `path` is within the token's
|
||||
/// scope for `{domain}:{action}:{path}`. For tokens without scope
|
||||
/// restrictions (no scopes at all, or only `if_jobs:filter_tags:*` scopes),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -258,7 +258,8 @@ async fn list_flows(
|
||||
r#"SELECT DISTINCT ON (path)
|
||||
path,
|
||||
value as "value!: sqlx::types::Json<Box<serde_json::value::RawValue>>",
|
||||
created_at
|
||||
created_at,
|
||||
email IS NULL as "legacy!"
|
||||
FROM draft
|
||||
WHERE workspace_id = $1
|
||||
AND typ = 'flow'
|
||||
@@ -315,9 +316,9 @@ async fn list_flows(
|
||||
inherited_labels: None,
|
||||
is_draft: true,
|
||||
draft_path,
|
||||
// Synthesized rows are the authed user's own draft.
|
||||
// Owned by nobody when legacy; see scripts.rs.
|
||||
draft_users: Some(sqlx::types::Json(vec![DraftUserRef {
|
||||
username: Some(authed.username.clone()),
|
||||
username: (!row.legacy).then(|| authed.username.clone()),
|
||||
}])),
|
||||
});
|
||||
}
|
||||
@@ -803,6 +804,24 @@ async fn create_flow(
|
||||
check_schedule_conflict(&mut tx, &w_id, &nf.path).await?;
|
||||
|
||||
let schema_str = nf.schema.and_then(|x| serde_json::to_string(&x.0).ok());
|
||||
let job_token_scopes = nf
|
||||
.job_token_scopes
|
||||
.as_ref()
|
||||
.and_then(|scopes| scopes.as_deref())
|
||||
.map(windmill_common::scopes::validate_job_token_scopes)
|
||||
.transpose()?;
|
||||
let restricts_steps = windmill_common::scopes::validate_flow_step_job_token_scopes(
|
||||
&serde_json::from_str::<windmill_common::flows::FlowValue>(nf.value.get())
|
||||
.map_err(|e| windmill_common::error::Error::BadRequest(e.to_string()))?,
|
||||
)?;
|
||||
if job_token_scopes.is_some() || restricts_steps {
|
||||
windmill_common::min_version::MIN_VERSION_SUPPORTS_JOB_TOKEN_SCOPES
|
||||
.assert()
|
||||
.await?;
|
||||
}
|
||||
if restricts_steps {
|
||||
windmill_common::feature_usage::log_feature_usage("job_token_scopes", "deploy", "step:set");
|
||||
}
|
||||
let resolved_on_behalf_of = windmill_common::resolve_on_behalf_of(
|
||||
nf.on_behalf_of_email.as_deref(),
|
||||
nf.on_behalf_of.as_deref(),
|
||||
@@ -823,14 +842,14 @@ async fn create_flow(
|
||||
dedicated_worker, visible_to_runner_only,
|
||||
ws_error_handler_muted,
|
||||
value, schema, edited_by, edited_at, labels,
|
||||
on_behalf_of, on_behalf_of_email
|
||||
on_behalf_of, on_behalf_of_email, job_token_scopes
|
||||
) VALUES (
|
||||
$1, $2, $3, $4,
|
||||
NULL, '', $5,
|
||||
$6, $7,
|
||||
$8,
|
||||
$9, $10::text::json, $11, now(), $12,
|
||||
$13, $14
|
||||
$13, $14, $15
|
||||
)"#,
|
||||
w_id,
|
||||
nf.path,
|
||||
@@ -846,9 +865,11 @@ async fn create_flow(
|
||||
nf.labels.as_deref() as Option<&[String]>,
|
||||
resolved_on_behalf_of,
|
||||
legacy_on_behalf_of_email,
|
||||
job_token_scopes.as_deref() as Option<&[String]>,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
windmill_common::scopes::log_job_token_scopes_deploy("flow", job_token_scopes.as_deref());
|
||||
|
||||
let version = sqlx::query_scalar!(
|
||||
"INSERT INTO flow_version (workspace_id, path, value, schema, created_by)
|
||||
@@ -973,6 +994,7 @@ async fn create_flow(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1152,7 +1174,7 @@ async fn get_flow_version(
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let flow = sqlx::query_as::<_, Flow>(
|
||||
"SELECT flow.workspace_id, flow.path, flow.summary, flow.description, flow.archived, flow.extra_perms, flow.dedicated_worker, flow.tag, flow.ws_error_handler_muted, flow.timeout, flow.visible_to_runner_only, flow.on_behalf_of, flow.labels, flow_version.schema, flow_version.value, flow_version.created_at as edited_at, flow_version.created_by as edited_by
|
||||
"SELECT flow.workspace_id, flow.path, flow.summary, flow.description, flow.archived, flow.extra_perms, flow.dedicated_worker, flow.tag, flow.ws_error_handler_muted, flow.timeout, flow.visible_to_runner_only, flow.on_behalf_of, flow.labels, flow.job_token_scopes, flow_version.schema, flow_version.value, flow_version.created_at as edited_at, flow_version.created_by as edited_by
|
||||
FROM flow
|
||||
LEFT JOIN flow_version ON flow_version.path = flow.path AND flow_version.workspace_id = flow.workspace_id
|
||||
WHERE flow.path = $1 AND flow.workspace_id = $2 AND flow_version.id = $3",
|
||||
@@ -1212,6 +1234,7 @@ async fn get_flow_version_by_id(
|
||||
flow.visible_to_runner_only,
|
||||
flow.on_behalf_of,
|
||||
flow.labels,
|
||||
flow.job_token_scopes,
|
||||
flow_version.schema,
|
||||
flow_version.value,
|
||||
flow_version.created_at as edited_at,
|
||||
@@ -1367,6 +1390,27 @@ async fn update_flow(
|
||||
let old_dep_job = not_found_if_none(old_dep_job, "Flow", flow_path)?;
|
||||
let is_new_path = nf.path != flow_path;
|
||||
let schema_str = schema.and_then(|x| serde_json::to_string(&x).ok());
|
||||
// Absent keeps the deployed value: a client unaware of the setting must not drop a
|
||||
// restriction by saving the flow.
|
||||
let set_job_token_scopes = nf.job_token_scopes.is_some();
|
||||
let job_token_scopes = nf
|
||||
.job_token_scopes
|
||||
.as_ref()
|
||||
.and_then(|scopes| scopes.as_deref())
|
||||
.map(windmill_common::scopes::validate_job_token_scopes)
|
||||
.transpose()?;
|
||||
let restricts_steps = windmill_common::scopes::validate_flow_step_job_token_scopes(
|
||||
&serde_json::from_str::<windmill_common::flows::FlowValue>(nf.value.get())
|
||||
.map_err(|e| windmill_common::error::Error::BadRequest(e.to_string()))?,
|
||||
)?;
|
||||
if job_token_scopes.is_some() || restricts_steps {
|
||||
windmill_common::min_version::MIN_VERSION_SUPPORTS_JOB_TOKEN_SCOPES
|
||||
.assert()
|
||||
.await?;
|
||||
}
|
||||
if restricts_steps {
|
||||
windmill_common::feature_usage::log_feature_usage("job_token_scopes", "deploy", "step:set");
|
||||
}
|
||||
let resolved_on_behalf_of = windmill_common::resolve_on_behalf_of(
|
||||
nf.on_behalf_of_email.as_deref(),
|
||||
nf.on_behalf_of.as_deref(),
|
||||
@@ -1401,7 +1445,8 @@ async fn update_flow(
|
||||
edited_at = now(),
|
||||
labels = COALESCE($13, labels),
|
||||
on_behalf_of = $14,
|
||||
on_behalf_of_email = $15
|
||||
on_behalf_of_email = $15,
|
||||
job_token_scopes = CASE WHEN $16 THEN $17 ELSE job_token_scopes END
|
||||
WHERE
|
||||
path = $11 AND workspace_id = $12",
|
||||
if is_new_path { flow_path } else { &nf.path },
|
||||
@@ -1419,19 +1464,24 @@ async fn update_flow(
|
||||
nf.labels.as_deref() as Option<&[String]>,
|
||||
resolved_on_behalf_of,
|
||||
legacy_on_behalf_of_email,
|
||||
set_job_token_scopes,
|
||||
job_token_scopes.as_deref() as Option<&[String]>,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error::Error::internal_err(format!("Error updating flow due to flow update: {e:#}"))
|
||||
})?;
|
||||
if set_job_token_scopes {
|
||||
windmill_common::scopes::log_job_token_scopes_deploy("flow", job_token_scopes.as_deref());
|
||||
}
|
||||
|
||||
if is_new_path {
|
||||
// if new path, must clone flow to new path and delete old flow for flow_version foreign key constraint
|
||||
sqlx::query!(
|
||||
"INSERT INTO flow
|
||||
(workspace_id, path, summary, description, archived, extra_perms, dependency_job, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at, labels)
|
||||
SELECT workspace_id, $1, summary, description, archived, extra_perms, dependency_job, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at, labels
|
||||
(workspace_id, path, summary, description, archived, extra_perms, dependency_job, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at, labels, job_token_scopes)
|
||||
SELECT workspace_id, $1, summary, description, archived, extra_perms, dependency_job, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at, labels, job_token_scopes
|
||||
FROM flow
|
||||
WHERE path = $2 AND workspace_id = $3",
|
||||
nf.path,
|
||||
@@ -1695,6 +1745,7 @@ async fn update_flow(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1870,6 +1921,7 @@ async fn get_flow_by_path(
|
||||
flow.visible_to_runner_only,
|
||||
flow.on_behalf_of,
|
||||
flow.labels,
|
||||
flow.job_token_scopes,
|
||||
folder_labels(flow.workspace_id, flow.path) AS inherited_labels,
|
||||
flow_version.id AS version_id,
|
||||
flow_version.schema,
|
||||
@@ -1911,6 +1963,7 @@ async fn get_flow_by_path(
|
||||
flow.visible_to_runner_only,
|
||||
flow.on_behalf_of,
|
||||
flow.labels,
|
||||
flow.job_token_scopes,
|
||||
folder_labels(flow.workspace_id, flow.path) AS inherited_labels,
|
||||
flow_version.id AS version_id,
|
||||
flow_version.schema,
|
||||
@@ -2335,6 +2388,7 @@ mod tests {
|
||||
apply_preprocessor: None,
|
||||
pass_flow_input_directly: None,
|
||||
debouncing: None,
|
||||
job_token_scopes: None,
|
||||
},
|
||||
FlowModule {
|
||||
id: "b".to_string(),
|
||||
@@ -2370,6 +2424,7 @@ mod tests {
|
||||
apply_preprocessor: None,
|
||||
pass_flow_input_directly: None,
|
||||
debouncing: None,
|
||||
job_token_scopes: None,
|
||||
},
|
||||
FlowModule {
|
||||
id: "c".to_string(),
|
||||
@@ -2405,6 +2460,7 @@ mod tests {
|
||||
apply_preprocessor: None,
|
||||
pass_flow_input_directly: None,
|
||||
debouncing: None,
|
||||
job_token_scopes: None,
|
||||
},
|
||||
],
|
||||
failure_module: Some(Box::new(FlowModule {
|
||||
@@ -2439,6 +2495,7 @@ mod tests {
|
||||
apply_preprocessor: None,
|
||||
pass_flow_input_directly: None,
|
||||
debouncing: None,
|
||||
job_token_scopes: None,
|
||||
})),
|
||||
preprocessor_module: None,
|
||||
same_worker: false,
|
||||
|
||||
@@ -232,3 +232,77 @@ async fn test_ai_proxy_x_resource_path_enforces_rls(db: Pool<Postgres>) -> anyho
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// An unsaved resource value sent in X-Resource-Value is what the upstream is called
|
||||
/// with, and a scoped token cannot use it to read a variable outside its scopes.
|
||||
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
|
||||
async fn test_ai_proxy_x_resource_value(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
use axum::{http::HeaderMap, http::StatusCode, routing::post, Json, Router};
|
||||
|
||||
initialize_tracing().await;
|
||||
std::env::set_var("ALLOW_PRIVATE_AI_BASE_URLS", "true");
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
// Only answers the key carried by the header, so another credential source fails.
|
||||
let app = Router::new().fallback(post(|headers: HeaderMap| async move {
|
||||
if headers.get("authorization").and_then(|v| v.to_str().ok()) == Some("Bearer sk-unsaved") {
|
||||
Ok(Json(json!({
|
||||
"id": "chatcmpl-test",
|
||||
"object": "chat.completion",
|
||||
"choices": [{"message": {"role": "assistant", "content": "hello"}}]
|
||||
})))
|
||||
} else {
|
||||
Err(StatusCode::UNAUTHORIZED)
|
||||
}
|
||||
}));
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
|
||||
let base_url = format!("http://127.0.0.1:{}/v1", listener.local_addr()?.port());
|
||||
tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO token (token_hash, token_prefix, token, email, label, super_admin, scopes) \
|
||||
VALUES (encode(sha256('SCOPED_AI_TOKEN'::bytea), 'hex'), 'SCOPED_AI_', 'SCOPED_AI_TOKEN', \
|
||||
'test@windmill.dev', 'scoped', true, ARRAY['ai:write', 'resources:write'])",
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let send = |api_key: &'static str, token: &'static str| {
|
||||
let value = json!({ "api_key": api_key, "base_url": base_url }).to_string();
|
||||
let encoded: String = value.bytes().map(|b| format!("%{b:02X}")).collect();
|
||||
authed_with(
|
||||
client()
|
||||
.post(format!(
|
||||
"http://localhost:{port}/api/w/test-workspace/ai/proxy/chat/completions"
|
||||
))
|
||||
.header("X-Provider", "openai")
|
||||
.header("X-Resource-Value", encoded)
|
||||
.json(&json!({
|
||||
"model": "gpt-4",
|
||||
"messages": [{"role": "user", "content": "hi"}]
|
||||
})),
|
||||
token,
|
||||
)
|
||||
.send()
|
||||
};
|
||||
|
||||
let resp = send("sk-unsaved", "SECRET_TOKEN").await?;
|
||||
assert_2xx(
|
||||
resp.status().as_u16(),
|
||||
&resp.text().await?,
|
||||
"X-Resource-Value with an unsaved resource",
|
||||
);
|
||||
|
||||
let resp = send("sk-other", "SECRET_TOKEN").await?;
|
||||
assert!(resp.status().as_u16() >= 400, "a different key must reach the upstream as is");
|
||||
|
||||
let resp = send("$var:u/test-user/secret", "SCOPED_AI_TOKEN").await?;
|
||||
let body = resp.text().await?;
|
||||
assert!(
|
||||
body.contains("variables:read:u/test-user/secret"),
|
||||
"a token without variables:read must not resolve a $var: reference, got: {body}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -200,3 +200,81 @@ async fn test_alert_job_queue_waiting_in_global_settings(db: Pool<Postgres>) ->
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_secret_surfaces(
|
||||
base: &str,
|
||||
names: &[&str],
|
||||
) -> anyhow::Result<Vec<(String, u16, String)>> {
|
||||
let mut paths = vec![
|
||||
"instance_config".to_string(),
|
||||
"instance_config/yaml".to_string(),
|
||||
];
|
||||
#[cfg(feature = "enterprise")]
|
||||
paths.push("list_global".to_string());
|
||||
paths.extend(names.iter().map(|n| format!("global/{n}")));
|
||||
let mut out = vec![];
|
||||
for path in paths {
|
||||
let resp = authed(client().get(format!("{base}/{path}")))
|
||||
.send()
|
||||
.await?;
|
||||
let status = resp.status().as_u16();
|
||||
out.push((path, status, resp.text().await?));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
|
||||
async fn test_server_secrets_follow_export_flag(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let secrets = [
|
||||
("jwt_secret", json!("planted-jwt-secret")),
|
||||
("rsa_keys", json!({"private_key": "planted-rsa-key"})),
|
||||
(
|
||||
"custom_instance_replication_pwd",
|
||||
json!("planted-replication-pwd"),
|
||||
),
|
||||
(
|
||||
"external_instance_pg_state",
|
||||
json!({"admin_pwd": "planted-pg-state"}),
|
||||
),
|
||||
];
|
||||
for (name, value) in &secrets {
|
||||
sqlx::query(
|
||||
"INSERT INTO global_settings (name, value) VALUES ($1, $2)
|
||||
ON CONFLICT (name) DO UPDATE SET value = EXCLUDED.value",
|
||||
)
|
||||
.bind(name)
|
||||
.bind(value)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
}
|
||||
let names: Vec<&str> = secrets.iter().map(|(n, _)| *n).collect();
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let base = format!("http://localhost:{}/api/settings", server.addr.port());
|
||||
|
||||
// Default: a full export, so `get-config` and `pull` can migrate an instance.
|
||||
std::env::remove_var("EXPORT_SERVER_SECRETS");
|
||||
for (path, status, body) in read_secret_surfaces(&base, &names).await? {
|
||||
assert_2xx(status, &body, &path);
|
||||
assert!(
|
||||
body.contains("planted-"),
|
||||
"{path} dropped a server secret: {body}"
|
||||
);
|
||||
}
|
||||
|
||||
std::env::set_var("EXPORT_SERVER_SECRETS", "0");
|
||||
let withheld = read_secret_surfaces(&base, &names).await;
|
||||
std::env::remove_var("EXPORT_SERVER_SECRETS");
|
||||
for (path, status, body) in withheld? {
|
||||
if path.starts_with("global/") {
|
||||
assert_eq!(status, 400, "GET {path} returned {status}");
|
||||
} else {
|
||||
assert_2xx(status, &body, &path);
|
||||
}
|
||||
assert!(
|
||||
!body.contains("planted-"),
|
||||
"{path} returned a server secret: {body}"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -756,6 +756,13 @@ async fn test_compare_workspaces_trigger_and_schedule(db: Pool<Postgres>) -> any
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
// The scheduler's missed-occurrence counters only ever move on the enabled side.
|
||||
sqlx::query(
|
||||
"UPDATE schedule SET late_run_streak = 1, missed_occurrences = 2, last_missed_at = NOW()
|
||||
WHERE workspace_id = 'test-workspace' AND path = 'f/sch/runtime_only'",
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
// ------ Schedule: config change (script_path) in fork. Should diff.
|
||||
sqlx::query!(
|
||||
@@ -2537,7 +2544,10 @@ async fn test_rename_records_the_vacated_path(db: Pool<Postgres>) -> anyhow::Res
|
||||
.await?;
|
||||
let status = resp.status();
|
||||
let hash = resp.text().await?;
|
||||
assert!(status.is_success(), "script deploy failed: {status} — {hash}");
|
||||
assert!(
|
||||
status.is_success(),
|
||||
"script deploy failed: {status} — {hash}"
|
||||
);
|
||||
parent_hash = Some(hash.trim().trim_matches('"').to_string());
|
||||
}
|
||||
let mut vacated = None;
|
||||
|
||||
@@ -124,7 +124,9 @@ pub async fn drop_unclaimable_run_lineage(
|
||||
|
||||
/// The jobs of `referenced` that `authed` cannot claim as its own run lineage: anything but the
|
||||
/// token's own job and that job's `parent_job`, `root_job` and `flow_innermost_root_job`, or for
|
||||
/// a workspace admin anything outside the workspace.
|
||||
/// a workspace admin anything outside the workspace. A restricted job token never gets the admin
|
||||
/// latitude: its flow-run routes trust this lineage (`job_in_same_flow_run`), so a claimed
|
||||
/// unrelated run would escape its scopes.
|
||||
pub async fn unclaimable_run_lineage(
|
||||
db: &DB,
|
||||
w_id: &str,
|
||||
@@ -157,7 +159,8 @@ pub async fn unclaimable_run_lineage(
|
||||
if referenced.is_empty() {
|
||||
return Ok(referenced);
|
||||
}
|
||||
let in_workspace = if authed.is_admin {
|
||||
let restricted_job_token = authed.job_id.is_some() && authed.scopes.is_some();
|
||||
let in_workspace = if authed.is_admin && !restricted_job_token {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT id FROM v2_job WHERE id = ANY($1) AND workspace_id = $2",
|
||||
&referenced,
|
||||
@@ -825,6 +828,7 @@ pub async fn handle_chat_conversation_messages(
|
||||
tx,
|
||||
memory_id,
|
||||
Some(job_id),
|
||||
job_id,
|
||||
&user_message,
|
||||
MessageType::User,
|
||||
None,
|
||||
@@ -865,6 +869,7 @@ pub async fn run_flow<'c>(
|
||||
chat_input_enabled,
|
||||
early_return,
|
||||
labels,
|
||||
job_token_scopes,
|
||||
..
|
||||
} = flow_version_info;
|
||||
|
||||
@@ -914,6 +919,7 @@ pub async fn run_flow<'c>(
|
||||
)
|
||||
};
|
||||
|
||||
let scope_ceiling = windmill_api_auth::caller_scope_ceiling(db, authed).await?;
|
||||
let (uuid, mut tx) = push(
|
||||
&db,
|
||||
tx,
|
||||
@@ -924,6 +930,7 @@ pub async fn run_flow<'c>(
|
||||
version,
|
||||
apply_preprocessor,
|
||||
labels,
|
||||
job_token_scopes,
|
||||
},
|
||||
push_args,
|
||||
authed.display_username(),
|
||||
@@ -950,6 +957,7 @@ pub async fn run_flow<'c>(
|
||||
None,
|
||||
authed.trigger_or_fallback(trigger),
|
||||
run_query.suspended_mode,
|
||||
scope_ceiling.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1141,6 +1149,7 @@ pub async fn push_script_job_by_path_into_queue<'c>(
|
||||
)
|
||||
};
|
||||
|
||||
let scope_ceiling = windmill_api_auth::caller_scope_ceiling(&db, &authed).await?;
|
||||
let (uuid, tx) = push(
|
||||
&db,
|
||||
tx,
|
||||
@@ -1176,6 +1185,7 @@ pub async fn push_script_job_by_path_into_queue<'c>(
|
||||
None,
|
||||
authed.trigger_or_fallback(trigger),
|
||||
run_query.suspended_mode,
|
||||
scope_ceiling.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -173,10 +173,33 @@ struct JobProgressSetRequest {
|
||||
}
|
||||
|
||||
async fn set_job_progress(
|
||||
authed: windmill_api_auth::ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, job_id)): Path<(String, Uuid)>,
|
||||
Json(JobProgressSetRequest { percent, flow_job_id }): Json<JobProgressSetRequest>,
|
||||
) -> error::JsonResult<()> {
|
||||
// A restricted job token reaches this route without a scope only for its own job
|
||||
// (`is_own_job_runtime_route`), so the flow it reports progress to must be its own too.
|
||||
if let (Some(flow_job_id), Some(token_job), Some(_)) =
|
||||
(flow_job_id, authed.job_id, &authed.scopes)
|
||||
{
|
||||
let own_flow = sqlx::query_scalar!(
|
||||
"SELECT $2 IN (parent_job, root_job, flow_innermost_root_job) FROM v2_job
|
||||
WHERE id = $1 AND workspace_id = $3",
|
||||
token_job,
|
||||
flow_job_id,
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.flatten()
|
||||
.unwrap_or(false);
|
||||
if !own_flow {
|
||||
return Err(error::Error::PermissionDenied(format!(
|
||||
"flow job {flow_job_id} is not this job's flow"
|
||||
)));
|
||||
}
|
||||
}
|
||||
// If flow_job_id exists, than we should modify flow_status of corresponding module
|
||||
// Individual jobs and flows are handled differently
|
||||
if let Some(flow_job_id) = flow_job_id {
|
||||
|
||||
@@ -112,6 +112,8 @@ pub struct ExportableQueuedJob {
|
||||
pub preprocessed: Option<bool>,
|
||||
pub args: Option<sqlx::types::Json<Box<RawValue>>>,
|
||||
pub labels: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub job_token_scopes: Option<Vec<String>>,
|
||||
pub pre_run_error: Option<String>,
|
||||
|
||||
// v2_job_queue columns (excluding workspace_id and id/created_at/tag/priority)
|
||||
@@ -276,6 +278,7 @@ pub async fn export_queued_jobs(
|
||||
v2_job.preprocessed,
|
||||
v2_job.args as "args: _",
|
||||
v2_job.labels,
|
||||
job_perms.job_token_scopes as "job_token_scopes?",
|
||||
v2_job.pre_run_error,
|
||||
|
||||
v2_job_queue.started_at,
|
||||
@@ -298,6 +301,7 @@ pub async fn export_queued_jobs(
|
||||
concurrency_key.key as "concurrency_key?"
|
||||
FROM v2_job_queue
|
||||
INNER JOIN v2_job ON v2_job.id = v2_job_queue.id
|
||||
LEFT JOIN job_perms ON job_perms.job_id = v2_job_queue.id
|
||||
LEFT JOIN v2_job_runtime ON v2_job_runtime.id = v2_job_queue.id
|
||||
LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id
|
||||
LEFT JOIN concurrency_key ON concurrency_key.job_id = v2_job_queue.id
|
||||
@@ -445,6 +449,23 @@ pub async fn import_queued_jobs(
|
||||
));
|
||||
}
|
||||
|
||||
// Imported rows bypass `push`, so nothing would cap their tokens: a restricted job token
|
||||
// could otherwise queue arbitrary jobs that run with its owner's full permissions.
|
||||
if authed.job_id.is_some() && authed.scopes.is_some() {
|
||||
return Err(error::Error::PermissionDenied(
|
||||
"A restricted job token cannot import queued jobs".to_string(),
|
||||
));
|
||||
}
|
||||
// An imported job gets no `job_perms` row, so its token would be minted without the
|
||||
// restriction it was queued with.
|
||||
if let Some(job) = jobs.iter().find(|job| job.job_token_scopes.is_some()) {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"Queued job {} restricts its job token and cannot be imported; run it again on \
|
||||
this instance instead",
|
||||
job.id
|
||||
)));
|
||||
}
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
for job in jobs {
|
||||
@@ -697,7 +718,8 @@ pub async fn delete_jobs(
|
||||
let mut conversation_ids: Vec<Uuid> = sqlx::query_scalar!(
|
||||
"DELETE FROM flow_conversation_message m
|
||||
USING flow_conversation c
|
||||
WHERE m.conversation_id = c.id AND c.workspace_id = $1 AND m.job_id = ANY($2)
|
||||
WHERE m.conversation_id = c.id AND c.workspace_id = $1
|
||||
AND COALESCE(m.turn_job_id, m.job_id) = ANY($2)
|
||||
RETURNING m.conversation_id",
|
||||
&w_id,
|
||||
&job_ids
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user