Merge remote-tracking branch 'origin/main' into glm/session-deployed-runs

This commit is contained in:
Guilhem Lemouel
2026-10-02 05:43:24 -07:00
46 changed files with 757 additions and 287 deletions
+3 -2
View File
@@ -35,7 +35,8 @@ env:
GLOBAL_EXTRA_MODELS: ${{ inputs.global_extra_models || 'gpt-6-astra gemini-3.8-flash' }}
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:
@@ -156,7 +157,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
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "1.820.0"
".": "1.821.0"
}
+40
View File
@@ -1,5 +1,45 @@
# Changelog
## [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)
+1 -1
View File
@@ -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" \
+89 -89
View File
@@ -7180,9 +7180,9 @@ dependencies = [
[[package]]
name = "lazy_static"
version = "1.5.0"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
dependencies = [
"spin 0.9.9",
]
@@ -9321,9 +9321,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pin-utils"
version = "0.1.0"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
checksum = "13bee6c73da26345c729282832b60b0363cf3dd9f4bfd81d8551b7a1c889a113"
[[package]]
name = "pinky-swear"
@@ -9870,9 +9870,9 @@ dependencies = [
[[package]]
name = "quinn-proto"
version = "0.11.18"
version = "0.11.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc"
checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe"
dependencies = [
"aws-lc-rs",
"bytes",
@@ -9893,9 +9893,9 @@ dependencies = [
[[package]]
name = "quinn-udp"
version = "0.5.15"
version = "0.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016"
dependencies = [
"cfg_aliases",
"libc",
@@ -14792,7 +14792,7 @@ dependencies = [
[[package]]
name = "windmill"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-nats",
@@ -14881,7 +14881,7 @@ dependencies = [
[[package]]
name = "windmill-ai"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"async-stream",
"async-trait",
@@ -14915,7 +14915,7 @@ dependencies = [
[[package]]
name = "windmill-alerting"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -14928,7 +14928,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"argon2",
@@ -15068,7 +15068,7 @@ dependencies = [
[[package]]
name = "windmill-api-agent-workers"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -15091,7 +15091,7 @@ dependencies = [
[[package]]
name = "windmill-api-assets"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -15108,7 +15108,7 @@ dependencies = [
[[package]]
name = "windmill-api-auth"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"axum 0.8.9",
@@ -15134,7 +15134,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -15144,7 +15144,7 @@ dependencies = [
[[package]]
name = "windmill-api-configs"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -15161,7 +15161,7 @@ dependencies = [
[[package]]
name = "windmill-api-debug"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"axum 0.8.9",
"base64 0.22.1",
@@ -15183,7 +15183,7 @@ dependencies = [
[[package]]
name = "windmill-api-embeddings"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"axum 0.8.9",
@@ -15206,7 +15206,7 @@ dependencies = [
[[package]]
name = "windmill-api-flow-conversations"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -15222,7 +15222,7 @@ dependencies = [
[[package]]
name = "windmill-api-flows"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -15244,7 +15244,7 @@ dependencies = [
[[package]]
name = "windmill-api-groups"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -15266,7 +15266,7 @@ dependencies = [
[[package]]
name = "windmill-api-inputs"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -15280,7 +15280,7 @@ dependencies = [
[[package]]
name = "windmill-api-integration-tests"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-nats",
@@ -15316,7 +15316,7 @@ dependencies = [
[[package]]
name = "windmill-api-jobs"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"axum 0.8.9",
@@ -15341,7 +15341,7 @@ dependencies = [
[[package]]
name = "windmill-api-npm-proxy"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -15369,7 +15369,7 @@ dependencies = [
[[package]]
name = "windmill-api-openapi"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"axum 0.8.9",
@@ -15391,7 +15391,7 @@ dependencies = [
[[package]]
name = "windmill-api-schedule"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -15411,7 +15411,7 @@ dependencies = [
[[package]]
name = "windmill-api-scripts"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"axum 0.8.9",
@@ -15453,7 +15453,7 @@ dependencies = [
[[package]]
name = "windmill-api-settings"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"axum 0.8.9",
@@ -15482,7 +15482,7 @@ dependencies = [
[[package]]
name = "windmill-api-sse"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"lazy_static",
"serde",
@@ -15494,7 +15494,7 @@ dependencies = [
[[package]]
name = "windmill-api-users"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"argon2",
"axum 0.8.9",
@@ -15518,7 +15518,7 @@ dependencies = [
[[package]]
name = "windmill-api-workers"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -15532,7 +15532,7 @@ dependencies = [
[[package]]
name = "windmill-api-workspaces"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -15571,7 +15571,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"chrono",
"lazy_static",
@@ -15585,7 +15585,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"axum 0.8.9",
@@ -15604,7 +15604,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"aes-gcm",
"aho-corasick",
@@ -15717,7 +15717,7 @@ dependencies = [
[[package]]
name = "windmill-dep-map"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"chrono",
"futures",
@@ -15737,7 +15737,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"regex",
"serde",
@@ -15754,7 +15754,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -15781,7 +15781,7 @@ dependencies = [
[[package]]
name = "windmill-jseval"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"futures",
@@ -15798,7 +15798,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -15814,7 +15814,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-trait",
@@ -15835,7 +15835,7 @@ dependencies = [
[[package]]
name = "windmill-native-triggers"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-trait",
@@ -15866,7 +15866,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"arc-swap",
@@ -15891,7 +15891,7 @@ dependencies = [
[[package]]
name = "windmill-object-store"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-stream",
@@ -15927,7 +15927,7 @@ dependencies = [
[[package]]
name = "windmill-operator"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"futures",
@@ -15945,7 +15945,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -15954,7 +15954,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15966,7 +15966,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"serde_json",
@@ -15978,7 +15978,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"gosyn",
@@ -15990,7 +15990,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16002,7 +16002,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16014,7 +16014,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -16025,7 +16025,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16036,7 +16036,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16048,7 +16048,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-asset"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -16059,7 +16059,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -16081,7 +16081,7 @@ dependencies = [
[[package]]
name = "windmill-parser-r"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16093,7 +16093,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16107,7 +16107,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -16124,7 +16124,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16137,7 +16137,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql-asset"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"serde",
@@ -16149,7 +16149,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16167,7 +16167,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts-asset"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"serde-wasm-bindgen",
@@ -16183,7 +16183,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wac"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -16199,7 +16199,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16213,7 +16213,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -16252,7 +16252,7 @@ dependencies = [
[[package]]
name = "windmill-runtime-nativets"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"const_format",
@@ -16292,7 +16292,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"getrandom 0.3.4",
"wasm-bindgen",
@@ -16303,7 +16303,7 @@ dependencies = [
[[package]]
name = "windmill-store"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -16338,7 +16338,7 @@ dependencies = [
[[package]]
name = "windmill-test-utils"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16362,7 +16362,7 @@ dependencies = [
[[package]]
name = "windmill-trigger"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16395,7 +16395,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-amqp"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16422,7 +16422,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-azure"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16455,7 +16455,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-email"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16475,7 +16475,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-gcp"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16509,7 +16509,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-http"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16546,7 +16546,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-kafka"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16569,7 +16569,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-mqtt"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16593,7 +16593,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-nats"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-nats",
@@ -16617,7 +16617,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-postgres"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16652,7 +16652,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-sqs"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16680,7 +16680,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-websocket"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16705,7 +16705,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"bitflags 2.13.2",
@@ -16724,7 +16724,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-once-cell",
@@ -16842,7 +16842,7 @@ dependencies = [
[[package]]
name = "windmill-worker-volumes"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"bytes",
"futures",
@@ -17539,9 +17539,9 @@ dependencies = [
[[package]]
name = "yoke-derive"
version = "0.8.3"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78"
checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71"
dependencies = [
"proc-macro2",
"quote",
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.820.0"
version = "1.821.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.821.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
+21 -1
View File
@@ -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,22 @@
"mpim:read",
"files:read"
],
"scope_options": [
{ "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)" }
],
"user_scope_options": [
{ "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" }
],
"token_response_path": "authed_user"
},
"supabase_wizard": {
@@ -100,6 +117,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" }
],
+24 -24
View File
@@ -6216,7 +6216,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill-common"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"aho-corasick",
"anyhow",
@@ -6302,7 +6302,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"proc-macro2",
"quote",
@@ -6314,7 +6314,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"convert_case",
"serde",
@@ -6323,7 +6323,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -6335,7 +6335,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"serde_json",
@@ -6347,7 +6347,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"gosyn",
@@ -6359,7 +6359,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -6371,7 +6371,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"serde_json",
@@ -6383,7 +6383,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -6394,7 +6394,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -6405,7 +6405,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -6417,7 +6417,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-asset"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -6428,7 +6428,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -6450,7 +6450,7 @@ dependencies = [
[[package]]
name = "windmill-parser-r"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"serde_json",
@@ -6462,7 +6462,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -6476,7 +6476,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"convert_case",
@@ -6493,7 +6493,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -6506,7 +6506,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql-asset"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"serde",
@@ -6518,7 +6518,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -6536,7 +6536,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts-asset"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"serde-wasm-bindgen",
@@ -6552,7 +6552,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wac"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -6568,7 +6568,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wasm"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"getrandom 0.2.17",
@@ -6600,7 +6600,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -6614,7 +6614,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.820.0"
version = "1.821.0"
dependencies = [
"anyhow",
"bitflags",
@@ -12,7 +12,7 @@ resolver = "2"
members = ["."]
[workspace.package]
version = "1.820.0"
version = "1.821.0"
edition = "2021"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
@@ -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(())
}
+33 -24
View File
@@ -26,7 +26,7 @@ use windmill_common::{
global_settings::{
load_value_from_global_settings, NPMRC_SETTING, NPM_CONFIG_REGISTRY_SETTING,
},
utils::{parse_npmrc_registry, StripPath},
utils::{parse_npmrc_registry, RegistryAuth, StripPath},
};
use windmill_api_auth::ApiAuthed;
@@ -149,7 +149,7 @@ pub fn workspaced_service() -> Router {
fn build_registry_request(
url: &str,
auth_token: &Option<String>,
auth: &Option<RegistryAuth>,
registry_base_url: &str,
) -> Result<reqwest::RequestBuilder> {
let parsed_url =
@@ -166,8 +166,17 @@ fn build_registry_request(
}
let mut req = HTTP_CLIENT.get(url);
if let Some(token) = auth_token {
req = req.bearer_auth(token);
// A packument may point a tarball at another port or plain http on the same host;
// credentials, often a real password, only go to the registry's own origin.
if parsed_url.origin() != parsed_base.origin() {
return Ok(req);
}
match auth {
Some(RegistryAuth::Bearer(token)) => req = req.bearer_auth(token),
Some(RegistryAuth::Basic(credentials)) => {
req = req.header(header::AUTHORIZATION, format!("Basic {credentials}"))
}
None => {}
}
Ok(req)
}
@@ -308,12 +317,12 @@ lazy_static::lazy_static! {
async fn fetch_package_json(
db: &sqlx::Pool<sqlx::Postgres>,
package: &str,
) -> Result<(Arc<JsonValue>, String, Option<String>)> {
let (registry_url, auth_token) = get_npm_registry(db)
) -> Result<(Arc<JsonValue>, String, Option<RegistryAuth>)> {
let (registry_url, auth) = get_npm_registry(db)
.await?
.ok_or_else(|| Error::BadRequest("No private npm registry configured".to_string()))?;
let document = fetch_package_json_from(&registry_url, &auth_token, package).await?;
Ok((document, registry_url, auth_token))
let document = fetch_package_json_from(&registry_url, &auth, package).await?;
Ok((document, registry_url, auth))
}
/// The same fetch against a registry the caller has already resolved.
@@ -325,7 +334,7 @@ async fn fetch_package_json(
/// immutable, so that survives switching back.
async fn fetch_package_json_from(
registry_url: &str,
auth_token: &Option<String>,
auth: &Option<RegistryAuth>,
package: &str,
) -> Result<Arc<JsonValue>> {
let registry_url = registry_url.to_string();
@@ -340,7 +349,7 @@ async fn fetch_package_json_from(
tracing::info!("Fetching package metadata from: {}", package_url);
let response = build_registry_request(&package_url, auth_token, &registry_url)?
let response = build_registry_request(&package_url, auth, &registry_url)?
.header(header::ACCEPT, ABBREVIATED_PACKUMENT)
.send()
.await
@@ -380,7 +389,7 @@ async fn tarball_response(
package: &str,
version: &str,
registry_url: &str,
auth_token: &Option<String>,
auth: &Option<RegistryAuth>,
) -> Result<reqwest::Response> {
let tarball_url = package_json
.get("versions")
@@ -390,7 +399,7 @@ async fn tarball_response(
.and_then(|t| t.as_str())
.ok_or_else(|| Error::NotFound(format!("Tarball not found for {}@{}", package, version)))?;
let response = build_registry_request(tarball_url, auth_token, registry_url)?
let response = build_registry_request(tarball_url, auth, registry_url)?
.send()
.await
.map_err(|e| Error::InternalErr(format!("Failed to download tarball: {}", e)))?;
@@ -411,9 +420,9 @@ async fn fetch_tarball(
package: &str,
version: &str,
registry_url: &str,
auth_token: &Option<String>,
auth: &Option<RegistryAuth>,
) -> Result<axum::body::Bytes> {
tarball_response(package_json, package, version, registry_url, auth_token)
tarball_response(package_json, package, version, registry_url, auth)
.await?
.bytes()
.await
@@ -642,10 +651,10 @@ async fn get_package_file(
Some(content) => content,
// Not a file a type request asks for, so it was never kept: walk the archive for it
None => {
let (package_json, registry_url, auth_token) =
let (package_json, registry_url, auth) =
fetch_package_json(&db, &package).await?;
let archive =
fetch_tarball(&package_json, &package, &version, &registry_url, &auth_token)
fetch_tarball(&package_json, &package, &version, &registry_url, &auth)
.await?;
let path = target.clone();
blocking(move || read_one_entry(&archive, &path))
@@ -668,13 +677,13 @@ async fn get_package_tarball(
Extension(db): Extension<sqlx::Pool<sqlx::Postgres>>,
) -> Result<Response> {
let (package, version) = parse_package_and_version(package_version_path.to_path())?;
let (package_json, registry_url, auth_token) = fetch_package_json(&db, &package).await?;
let (package_json, registry_url, auth) = fetch_package_json(&db, &package).await?;
let response = tarball_response(
&package_json,
&package,
&version,
&registry_url,
&auth_token,
&auth,
)
.await?;
@@ -693,11 +702,11 @@ async fn get_package_tarball(
.into_response())
}
/// Get the npm registry URL and optional auth token from global settings.
/// Get the npm registry URL and optional credentials from global settings.
/// Checks the `npmrc` setting first, then falls back to `npm_config_registry`.
async fn get_npm_registry(
db: &sqlx::Pool<sqlx::Postgres>,
) -> Result<Option<(String, Option<String>)>> {
) -> Result<Option<(String, Option<RegistryAuth>)>> {
let npmrc = load_value_from_global_settings(db, NPMRC_SETTING)
.await?
.and_then(|v| v.as_str().map(|s| s.to_string()));
@@ -716,7 +725,7 @@ async fn get_npm_registry(
let (url, token) = if s.contains(":_authToken=") {
let parts: Vec<&str> = s.split(":_authToken=").collect();
let url = parts[0].to_string();
let token = parts.get(1).map(|t| t.to_string());
let token = parts.get(1).map(|t| RegistryAuth::Bearer(t.to_string()));
(url, token)
} else {
(s.clone(), None)
@@ -941,7 +950,7 @@ async fn cached_package(
) -> Result<PackageFiles> {
// Resolving the registry is a settings read; the packument behind it is a round trip,
// and a version already on disk needs neither, so it is fetched only on a miss.
let (registry_url, auth_token) = get_npm_registry(db)
let (registry_url, auth) = get_npm_registry(db)
.await?
.ok_or_else(|| Error::BadRequest("No private npm registry configured".to_string()))?;
@@ -963,9 +972,9 @@ async fn cached_package(
}
// The same snapshot the cache key came from, not a second read of the setting.
let package_json = fetch_package_json_from(&registry_url, &auth_token, package).await?;
let package_json = fetch_package_json_from(&registry_url, &auth, package).await?;
let archive =
fetch_tarball(&package_json, package, version, &registry_url, &auth_token).await?;
fetch_tarball(&package_json, package, version, &registry_url, &auth).await?;
let target = dir.clone();
let extracted = blocking(move || {
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.820.0
version: 1.821.0
title: Windmill API
contact:
+40
View File
@@ -1062,6 +1062,16 @@ where
}
}
/// Paths of every `$var:` reference found anywhere in a resource value.
fn var_references(value: &serde_json::Value) -> Vec<&str> {
match value {
serde_json::Value::String(s) => s.strip_prefix("$var:").into_iter().collect(),
serde_json::Value::Array(items) => items.iter().flat_map(var_references).collect(),
serde_json::Value::Object(fields) => fields.values().flat_map(var_references).collect(),
_ => vec![],
}
}
async fn global_proxy(
authed: ApiAuthed,
Extension(db): Extension<DB>,
@@ -1220,7 +1230,37 @@ async fn proxy(
// Set when serving the request through Windmill's free AI tier (the lent key). Holds
// the per-user concurrency lock and drives response metering.
let mut free_lease: Option<crate::ai_free_tier_oss::FreeTierLease> = None;
// An unsaved resource value (percent-encoded JSON), so a resource can be tested
// before it is stored. `$var:` references resolve as the caller, and since the
// caller also picks the base URL they are sent to, a scoped token needs the same
// scopes as storing the resource and reading each variable would.
let inline_resource = headers
.get("X-Resource-Value")
.map(|v| {
let invalid = |e: String| Error::BadRequest(format!("Invalid X-Resource-Value: {e}"));
let decoded =
urlencoding::decode(v.to_str().unwrap_or("")).map_err(|e| invalid(e.to_string()))?;
let value = serde_json::from_str::<serde_json::Value>(&decoded)
.map_err(|e| invalid(e.to_string()))?;
check_scopes(&authed, || "resources:write".to_string())?;
for path in var_references(&value) {
check_scopes(&authed, || format!("variables:read:{path}"))?;
}
serde_json::from_value::<AIResource>(value).map_err(|e| invalid(e.to_string()))
})
.transpose()?;
let mut credentials = 'cred: {
if let Some(resource) = inline_resource {
break 'cred resolve_provider_credentials(
&provider,
&db,
&w_id,
resource,
Some(&authed),
)
.await?;
}
match workspace_cache {
Some(request_cache)
if !request_cache.is_expired() && forced_resource_path.is_none() =>
+146 -17
View File
@@ -2206,11 +2206,30 @@ mod tests {
}
}
/// Parse .npmrc content to extract the default registry URL and its auth token.
/// Returns `Some((registry_url, Option<auth_token>))` if a default registry is found.
pub fn parse_npmrc_registry(npmrc_content: &str) -> Option<(String, Option<String>)> {
/// Credentials for a registry, as an `Authorization` header expects them.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RegistryAuth {
Bearer(String),
/// Already base64 encoded `user:password`.
Basic(String),
}
/// Parse .npmrc content to extract the default registry URL and its credentials.
/// Returns `Some((registry_url, Option<auth>))` if a default registry is found.
///
/// Credentials follow npm's precedence for a registry: `_authToken`, then `_auth`, then
/// `username` with a base64 encoded `_password`.
pub fn parse_npmrc_registry(npmrc_content: &str) -> Option<(String, Option<RegistryAuth>)> {
use base64::{
alphabet,
engine::{
general_purpose::STANDARD, DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig,
},
Engine,
};
let mut registry_url: Option<String> = None;
let mut auth_tokens: Vec<(String, String)> = Vec::new();
let mut scoped_keys: Vec<(String, String, String)> = Vec::new();
for line in npmrc_content.lines() {
let line = line.trim();
@@ -2223,8 +2242,15 @@ pub fn parse_npmrc_registry(npmrc_content: &str) -> Option<(String, Option<Strin
}
if line.starts_with("//") {
if let Some((prefix, token)) = line.split_once(":_authToken=") {
auth_tokens.push((prefix.to_string(), token.to_string()));
if let Some((prefix, key, value)) = line
.split_once('=')
.and_then(|(lhs, value)| lhs.rsplit_once(':').map(|(p, k)| (p, k, value)))
{
scoped_keys.push((
prefix.trim_end_matches('/').to_string(),
key.trim().to_string(),
value.trim().to_string(),
));
}
}
}
@@ -2233,20 +2259,65 @@ pub fn parse_npmrc_registry(npmrc_content: &str) -> Option<(String, Option<Strin
let url_without_protocol = url.trim_start_matches("https:").trim_start_matches("http:");
let url_prefix = url_without_protocol.trim_end_matches('/');
let token = auth_tokens
.iter()
.find(|(prefix, _)| {
let p = prefix.trim_end_matches('/');
p == url_prefix
})
.map(|(_, token)| token.clone());
// As in npm's config, the last assignment wins, and an empty one unsets the key.
let get = |prefix: &str, key: &str| {
scoped_keys
.iter()
.rev()
.find(|(p, k, _)| p == prefix && k == key)
.map(|(_, _, value)| value.clone())
.filter(|value| !value.is_empty())
};
let auth_at = |prefix: &str| {
if let Some(token) = get(prefix, "_authToken") {
Some(RegistryAuth::Bearer(token))
} else if let Some(auth) = get(prefix, "_auth") {
Some(RegistryAuth::Basic(auth))
} else if let (Some(username), Some(password)) =
(get(prefix, "username"), get(prefix, "_password"))
{
let lenient = GeneralPurpose::new(
&alphabet::STANDARD,
GeneralPurposeConfig::new()
.with_decode_padding_mode(DecodePaddingMode::Indifferent),
);
match lenient
.decode(&password)
.ok()
.and_then(|p| String::from_utf8(p).ok())
{
Some(password) => Some(RegistryAuth::Basic(
STANDARD.encode(format!("{username}:{password}")),
)),
None => {
tracing::warn!("npmrc _password for {prefix} is not base64, ignoring it");
None
}
}
} else {
None
}
};
Some((url, token))
// Like npm, the deepest path of the registry URL that carries credentials supplies them,
// walking up to the bare host.
let mut prefix = url_prefix;
let auth = loop {
if let Some(auth) = auth_at(prefix) {
break Some(auth);
}
match prefix.rfind('/') {
Some(i) if i > 1 => prefix = &prefix[..i],
_ => break None,
}
};
Some((url, auth))
}
#[cfg(test)]
mod npmrc_tests {
use super::parse_npmrc_registry;
use super::{parse_npmrc_registry, RegistryAuth};
#[test]
fn test_parse_simple_registry() {
@@ -2256,7 +2327,7 @@ mod npmrc_tests {
result,
Some((
"https://registry.mycompany.com/".to_string(),
Some("secret123".to_string())
Some(RegistryAuth::Bearer("secret123".to_string()))
))
);
}
@@ -2287,11 +2358,69 @@ mod npmrc_tests {
result,
Some((
"https://r.example.com/".to_string(),
Some("tok".to_string())
Some(RegistryAuth::Bearer("tok".to_string()))
))
);
}
#[test]
fn test_parse_username_password() {
// "cGFzcw==" is base64("pass"); the header value is base64("user:pass")
let npmrc = "registry=https://pkgs.dev.azure.com/org/_packaging/feed/npm/registry/\n\
//pkgs.dev.azure.com/org/_packaging/feed/npm/registry/:username=user\n\
//pkgs.dev.azure.com/org/_packaging/feed/npm/registry/:_password=cGFzcw==\n\
//pkgs.dev.azure.com/org/_packaging/feed/npm/registry/:email=npm requires email\n";
let (_, auth) = parse_npmrc_registry(npmrc).unwrap();
assert_eq!(auth, Some(RegistryAuth::Basic("dXNlcjpwYXNz".to_string())));
}
#[test]
fn test_parse_auth_and_precedence() {
let base = "registry=https://r.example.com/\n";
let auth = "//r.example.com/:_auth=dXNlcjpwYXNz\n";
let user_pass = "//r.example.com/:username=other\n//r.example.com/:_password=eA==\n";
let token = "//r.example.com:_authToken=tok\n";
let parse = |s: String| parse_npmrc_registry(&s).unwrap().1;
assert_eq!(
parse(format!("{base}{auth}")),
Some(RegistryAuth::Basic("dXNlcjpwYXNz".to_string()))
);
assert_eq!(
parse(format!("{base}{user_pass}{auth}")),
Some(RegistryAuth::Basic("dXNlcjpwYXNz".to_string()))
);
assert_eq!(
parse(format!("{base}{auth}{user_pass}{token}")),
Some(RegistryAuth::Bearer("tok".to_string()))
);
// Credentials for another registry are not sent to the default one
assert_eq!(parse(format!("{base}//other.example.com/:_auth=x\n")), None);
// An empty value is unset, and a later line overrides an earlier one
assert_eq!(
parse(format!(
"{base}//r.example.com/:_authToken=\n{auth}//r.example.com/:_auth=y\n"
)),
Some(RegistryAuth::Basic("y".to_string()))
);
// An empty assignment clears an earlier value rather than being skipped
assert_eq!(
parse(format!("{base}{auth}//r.example.com/:_auth=\n{user_pass}")),
Some(RegistryAuth::Basic("b3RoZXI6eA==".to_string()))
);
}
#[test]
fn test_parse_auth_from_parent_path() {
let npmrc = "registry=https://r.example.com:4873/npm/feed/\n\
//r.example.com:4873/:_authToken=host\n\
//r.example.com:4873/npm/:username=user\n\
//r.example.com:4873/npm/:_password=cGFzcw\n";
// The deepest path with credentials wins; the unpadded password still decodes
let (_, auth) = parse_npmrc_registry(npmrc).unwrap();
assert_eq!(auth, Some(RegistryAuth::Basic("dXNlcjpwYXNz".to_string())));
}
#[test]
fn test_parse_empty_npmrc() {
assert_eq!(parse_npmrc_registry(""), None);
+3 -2
View File
@@ -92,8 +92,9 @@ pub struct OAuthConfig {
/// The registry JSON may also carry frontend-only keys for the connect
/// dialog, deliberately not modelled here: `scope_options`, a scope pick
/// list, `resource_fields`, the fields of the resource type the dialog
/// asks for once the token is in (Snowflake's database and warehouse), and
/// `user_scopes`, Slack's user-token scopes, sent as `user_scope`.
/// asks for once the token is in (Snowflake's database and warehouse),
/// `user_scopes`, Slack's user-token scopes, sent as `user_scope`, and
/// `user_scope_options`, the pick list offered alongside `user_scopes`.
pub scopes: Option<Vec<String>>,
/// Default scopes for the client-credentials (2-legged) flow. These differ
/// from the authorization-code `scopes` for most providers (member/consent
+1 -1
View File
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.820.0";
export const VERSION = "v1.821.0";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-chat",
"version": "1.820.0",
"version": "1.821.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-chat",
"version": "1.820.0",
"version": "1.821.0",
"license": "Apache-2.0",
"devDependencies": {
"@ai-sdk/react": "^4.0.102",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "windmill-chat",
"description": "Build chat interfaces on Windmill flows deployed in chat mode, from any frontend or raw app",
"version": "1.820.0",
"version": "1.821.0",
"author": "Ruben Fiszel",
"license": "Apache-2.0",
"homepage": "https://github.com/windmill-labs/windmill/tree/main/chat-sdk#readme",
+65 -1
View File
@@ -11,6 +11,7 @@ import { psql as psqlDatatable } from "./psql.ts";
import { serve as serveDatatable } from "./serve.ts";
import {
createMigration,
listLocalMigrations,
pushLocalMigrations,
rollbackMigrations,
runMigrations,
@@ -83,6 +84,59 @@ async function migrateDown(opts: GlobalOptions & { datatable?: string }) {
await rollbackMigrations(workspace.workspaceId, dt);
}
async function migrateStatus(
opts: GlobalOptions & { datatable?: string; json?: boolean },
) {
if (opts.json) log.setSilent(true);
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
const dt = opts.datatable ?? DEFAULT_DATATABLE_NAME;
const status = await wmill.getDatatableMigrationsStatus({
workspace: workspace.workspaceId,
datatableName: dt,
});
// `migrate up` pushes local files before running, so they count as pending too.
const remote = new Set(status.migrations.map((m) => m.timestamp));
const migrations: { timestamp: number; name: string; status: string }[] = [
...status.migrations,
...listLocalMigrations(dt)
.filter((m) => !remote.has(m.timestamp))
.map((m) => ({ ...m, status: "local" })),
].sort((a, b) => a.timestamp - b.timestamp);
if (opts.json) {
console.log(JSON.stringify({ ...status, migrations }));
return;
}
if (!status.enabled) {
log.info(`Migrations are not enabled on datatable '${dt}'`);
return;
}
if (status.error) {
log.warn(`Could not read applied migrations on '${dt}': ${status.error}`);
}
if (migrations.length === 0) {
log.info(`No migrations on datatable '${dt}'`);
return;
}
const label: Record<string, string> = {
not_run: "pending",
local: "pending (not pushed)",
};
new Table()
.header(["Timestamp", "Name", "Status"])
.padding(2)
.border(true)
.body(
migrations.map((m) => [
String(m.timestamp),
m.name,
label[m.status] ?? m.status,
]),
)
.render();
}
const migrateCommand = new Command()
.description("manage datatable migrations")
.command("new", "scaffold a new migration (.up.sql / .down.sql files)")
@@ -109,7 +163,17 @@ const migrateCommand = new Command()
"-d --datatable <datatable:string>",
"Target datatable (default: main)",
)
.action(migrateDown as any);
.action(migrateDown as any)
.command(
"status",
"show applied and pending migrations on the main datatable (or one via --datatable)",
)
.option(
"-d --datatable <datatable:string>",
"Target datatable (default: main)",
)
.option("--json", "Output as JSON (for piping to jq)")
.action(migrateStatus as any);
type DataTableResourceType = "postgresql" | "instance" | "external_instance";
+18 -9
View File
@@ -242,6 +242,23 @@ export async function pushMigrationFromDisk(
});
}
/**
* The local migrations of a data table, identified by their `.up.sql` file (the
* up file is mandatory); files only deleted locally are deliberately ignored.
*/
export function listLocalMigrations(
datatableName: string,
): { timestamp: number; name: string }[] {
const dir = path.join(process.cwd(), MIGRATIONS_DIR, datatableName);
if (!fs.existsSync(dir)) return [];
const local: { timestamp: number; name: string }[] = [];
for (const file of fs.readdirSync(dir)) {
const m = file.match(/^(\d+)_(.*)\.up\.sql$/);
if (m) local.push({ timestamp: Number(m[1]), name: m[2] });
}
return local;
}
/**
* Upsert the on-disk migrations of a data table to the workspace, so a freshly
* created migration file works with `wmill datatable migrate up` even without a
@@ -254,15 +271,7 @@ export async function pushLocalMigrations(
datatableName: string,
): Promise<void> {
const dir = path.join(process.cwd(), MIGRATIONS_DIR, datatableName);
if (!fs.existsSync(dir)) return;
// Local migrations are identified by their `.up.sql` file (the up file is
// mandatory); this deliberately ignores files that were only deleted locally.
const local: { timestamp: number; name: string }[] = [];
for (const file of fs.readdirSync(dir)) {
const m = file.match(/^(\d+)_(.*)\.up\.sql$/);
if (m) local.push({ timestamp: Number(m[1]), name: m[2] });
}
const local = listLocalMigrations(datatableName);
if (local.length === 0) return;
const remote = await wmill.listDatatableMigrations({ workspace });
+1 -1
View File
@@ -10,4 +10,4 @@ export const WM_FORK_PREFIX = "wm-fork";
// (e.g. utils.ts) can read it without importing main.ts and creating a circular
// dependency (main → workspace → utils → main) that triggers a TDZ.
// Re-exported from main.ts for backwards compatibility.
export const VERSION = "1.820.0";
export const VERSION = "1.821.0";
+3
View File
@@ -7916,6 +7916,9 @@ datatable related commands
- \`-d --datatable <datatable:string>\` - Target datatable (default: main)
- \`datatable migrate down\` - roll back the most recent migration on the main datatable (or one via --datatable)
- \`-d --datatable <datatable:string>\` - Target datatable (default: main)
- \`datatable migrate status\` - show applied and pending migrations on the main datatable (or one via --datatable)
- \`-d --datatable <datatable:string>\` - Target datatable (default: main)
- \`--json\` - Output as JSON (for piping to jq)
- \`datatable create [name:string]\` - register a datatable database in the workspace (default: instance-backed 'main') so scripts can use datatable://<name>
- \`--resource <resource:string>\` - Back the datatable with an existing postgresql resource path instead of the instance database
- \`--force\` - Allow adding to a workspace that already has datatables (fork metadata on existing ones is not preserved)
+1 -1
View File
@@ -96,7 +96,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" \
+1 -1
View File
@@ -96,7 +96,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" \
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@windmill-labs/components",
"version": "1.820.0",
"version": "1.821.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@windmill-labs/components",
"version": "1.820.0",
"version": "1.821.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@windmill-labs/components",
"version": "1.820.0",
"version": "1.821.0",
"scripts": {
"dev": "vite dev",
"dev:ui-builder": "mv static/ui_builder static/ui_builder.dev-disabled 2>/dev/null || true ; trap 'mv static/ui_builder.dev-disabled static/ui_builder 2>/dev/null || true' EXIT ; vite dev",
@@ -5,6 +5,9 @@
import SchemaForm from './SchemaForm.svelte'
import Toggle from './Toggle.svelte'
import TestConnection from './TestConnection.svelte'
import TestAiKey from './copilot/TestAIKey.svelte'
import { aiResourceProvider } from './copilot/aiResourceProvider'
import { copilotInfo } from '$lib/aiStore'
import Popover from './meltComponents/Popover.svelte'
import Button from './common/button/Button.svelte'
import { Loader2 } from 'lucide-svelte'
@@ -51,6 +54,8 @@
onSynced = undefined
}: Props = $props()
let aiProvider = $derived(aiResourceProvider(resourceType))
let schema = $state(emptySchema())
let notFound = $state(false)
@@ -180,6 +185,14 @@
isFileset={resourceTypeInfo?.is_fileset ?? false}
/>
<TestConnection {resourceType} {args} />
{#if aiProvider}
<TestAiKey
{aiProvider}
workspace={workspace ?? $operatingWorkspace}
resourceValue={args}
model={$copilotInfo.aiModels.find((m) => m.provider === aiProvider)?.model}
/>
{/if}
{#if resourceType == 'postgresql'}
<Popover
floatingConfig={{
@@ -278,7 +278,9 @@
let scopeOptions = $derived(
useClientCredentials
? defaultCcScopes()
: [...(registryEntry()?.scope_options ?? []), ...instanceScopes]
: useUserToken
? [...(registryEntry()?.user_scope_options ?? []), ...(registryEntry()?.user_scopes ?? [])]
: [...(registryEntry()?.scope_options ?? []), ...instanceScopes]
)
/** Their slice of the resource type's schema, so they render with the type's own
@@ -334,6 +336,10 @@
ccInstance = ''
tokenUrl = ''
scopes = []
if (description === generatedDescription) {
description = ''
}
generatedDescription = ''
}
/** Default scopes for the client-credentials grant. Registry providers use
@@ -14,6 +14,9 @@
import FilesetEditor from './FilesetEditor.svelte'
import Toggle from './Toggle.svelte'
import TestConnection from './TestConnection.svelte'
import TestAiKey from './copilot/TestAIKey.svelte'
import { aiResourceProvider } from './copilot/aiResourceProvider'
import { copilotInfo } from '$lib/aiStore'
import GfmMarkdown from './GfmMarkdown.svelte'
import TestTriggerConnection from './triggers/TestTriggerConnection.svelte'
import GitHubAppIntegration from './GitHubAppIntegration.svelte'
@@ -85,6 +88,7 @@
}: Props = $props()
let ws = $derived(workspace ?? $operatingWorkspace)
let aiProvider = $derived(aiResourceProvider(resource_type))
let rawCode: string | undefined = $state(undefined)
let textFileContent: string = $state('')
@@ -225,6 +229,14 @@
workspaceOverride={workspace}
/>
{/if}
{#if aiProvider}
<TestAiKey
{aiProvider}
workspace={ws}
resourceValue={args}
model={$copilotInfo.aiModels.find((m) => m.provider === aiProvider)?.model}
/>
{/if}
{#if resource_type === 'git_repository' && ws && (actingUser?.is_admin || actingUser?.is_super_admin)}
<GitHubAppIntegration
resourceType={resource_type}
@@ -2,15 +2,19 @@
import type { AIProvider } from '$lib/gen'
import { sendUserToast } from '$lib/toast'
import Button from '../common/button/Button.svelte'
import { testKey } from './lib'
import TextInput from '../text_input/TextInput.svelte'
import { AI_PROVIDERS, testKey } from './lib'
import { untrack } from 'svelte'
interface Props {
disabled?: boolean
apiKey?: string | undefined
workspace?: string | undefined
resourcePath?: string | undefined
/** An unsaved resource value to test as is, instead of a stored resource. */
resourceValue?: Record<string, any> | undefined
aiProvider: AIProvider
model: string
model?: string | undefined
}
let {
@@ -18,50 +22,69 @@
apiKey = undefined,
workspace = undefined,
resourcePath = undefined,
resourceValue = undefined,
aiProvider,
model
model = undefined
}: Props = $props()
let loading = $state(false)
// For an unsaved resource, or without a model from the caller, the model is only a
// guess: deployment-named providers (Azure, Bedrock, custom endpoints) reject a model
// the account lacks, which would read as an invalid key, so the guess stays editable.
let typedModel = $state(
untrack(() => model ?? AI_PROVIDERS[aiProvider]?.defaultModels[0] ?? '')
)
let modelEditable = $derived(!!resourceValue || !model)
let testedModel = $derived(modelEditable ? typedModel.trim() : model)
</script>
<Button
unifiedSize="md"
variant="default"
{disabled}
{loading}
on:click={async () => {
loading = true
try {
const abortController = new AbortController()
setTimeout(() => {
abortController.abort()
}, 10000)
<div class="flex flex-row items-center gap-1">
{#if modelEditable}
<TextInput
bind:value={typedModel}
size="md"
inputProps={{ placeholder: 'Model to test', 'aria-label': 'Model to test' }}
/>
{/if}
<Button
unifiedSize="md"
variant="default"
disabled={disabled || !testedModel}
{loading}
onClick={async () => {
loading = true
try {
const abortController = new AbortController()
setTimeout(() => {
abortController.abort()
}, 10000)
await testKey({
apiKey,
workspace,
resourcePath,
messages: [
{
role: 'user',
content: "this is a test, simply reply with 'ok'"
}
],
abortController,
aiProvider,
model
})
sendUserToast('Valid key')
} catch (err) {
if (err.message === 'Request was aborted.') {
sendUserToast('Could not validate key within 10s', true)
} else {
sendUserToast(`Invalid key: ${err}`, true)
await testKey({
apiKey,
workspace,
resourcePath,
resourceValue,
messages: [
{
role: 'user',
content: "this is a test, simply reply with 'ok'"
}
],
abortController,
aiProvider,
model: testedModel
})
sendUserToast('Valid key')
} catch (err) {
if (err.message === 'Request was aborted.') {
sendUserToast('Could not validate key within 10s', true)
} else {
sendUserToast(`Invalid key: ${err}`, true)
}
} finally {
loading = false
}
} finally {
loading = false
}
}}
>Test key
</Button>
}}
>Test key
</Button>
</div>
@@ -0,0 +1,10 @@
import type { AIProvider } from '$lib/gen'
import { AI_PROVIDERS } from './lib'
/** The AI provider a resource of this type holds credentials for, if any. */
export function aiResourceProvider(resourceType: string | undefined): AIProvider | undefined {
if (resourceType === 'openai_client_credentials_oauth') return 'openai'
return resourceType && Object.hasOwn(AI_PROVIDERS, resourceType)
? (resourceType as AIProvider)
: undefined
}
@@ -10,6 +10,8 @@ import {
getAiProxyBaseURL,
getProviderAndCompletionConfig,
providerSupportsWebSearch,
testedCredentialHeaders,
usesGlobalAiProxy,
workspaceAIClients
} from '../lib'
import { applyReasoningToConfig } from '../reasoningRegistry'
@@ -624,6 +626,7 @@ export async function getNonStreamingOpenAIResponsesCompletion(
apiKey?: string
workspace?: string
resourcePath?: string
resourceValue?: Record<string, any>
forceModelProvider?: AIProviderModel
maxTokensCap?: number
}
@@ -645,23 +648,12 @@ export async function getNonStreamingOpenAIResponsesCompletion(
} = {
signal: abortController.signal,
headers: {
'X-Provider': provider
'X-Provider': provider,
...testedCredentialHeaders(options ?? {})
}
}
if (options?.resourcePath) {
fetchOptions.headers = {
...fetchOptions.headers,
'X-Resource-Path': options.resourcePath
}
} else if (options?.apiKey) {
fetchOptions.headers = {
...fetchOptions.headers,
'X-API-Key': options.apiKey
}
}
const openaiClient = options?.apiKey
const openaiClient = usesGlobalAiProxy(options ?? {})
? createOpenAIProxyClient(getAiProxyBaseURL())
: options?.workspace
? workspaceAIClients.createOpenaiClient(options.workspace)
+44 -25
View File
@@ -581,10 +581,39 @@ class WorkspacedAIClients {
export const workspaceAIClients = new WorkspacedAIClients()
interface TestedCredential {
apiKey?: string
resourcePath?: string
resourceValue?: Record<string, any>
}
/** The header naming the credential under test; a resource wins over a bare key. */
export function testedCredentialHeaders({
apiKey,
resourcePath,
resourceValue
}: TestedCredential): Record<string, string> {
if (resourceValue) {
// Header values must be ASCII, the resource may not be.
return { 'X-Resource-Value': encodeURIComponent(JSON.stringify(resourceValue)) }
} else if (resourcePath) {
return { 'X-Resource-Path': resourcePath }
} else if (apiKey) {
return { 'X-API-Key': apiKey }
}
return {}
}
/** A bare key has no workspace resource behind it, so it goes through the global proxy. */
export function usesGlobalAiProxy({ apiKey, resourcePath, resourceValue }: TestedCredential) {
return !!apiKey && !resourcePath && !resourceValue
}
export async function testKey({
apiKey,
workspace,
resourcePath,
resourceValue,
model,
abortController,
messages,
@@ -593,13 +622,14 @@ export async function testKey({
apiKey?: string
workspace?: string
resourcePath?: string
resourceValue?: Record<string, any>
model: string | undefined
messages: ChatCompletionMessageParam[]
abortController: AbortController
aiProvider: AIProvider
}) {
if (!apiKey && !resourcePath) {
throw new Error('API key or resource path is required')
if (!apiKey && !resourcePath && !resourceValue) {
throw new Error('API key, resource path or resource value is required')
}
const modelToTest = model ?? AI_PROVIDERS[aiProvider].defaultModels[0]
@@ -617,6 +647,7 @@ export async function testKey({
apiKey,
workspace,
resourcePath,
resourceValue,
forceModelProvider: {
model: modelToTest,
provider: aiProvider
@@ -638,6 +669,7 @@ interface AnthropicCompletionParams {
apiKey?: string
workspace?: string
resourcePath?: string
resourceValue?: Record<string, any>
maxTokensCap?: number
}
@@ -647,6 +679,7 @@ function buildAnthropicProxyRequest({
apiKey,
workspace,
resourcePath,
resourceValue,
maxTokensCap
}: Omit<AnthropicCompletionParams, 'abortController'>) {
const { system, messages: anthropicMessages } = convertOpenAIToAnthropicMessages(messages)
@@ -655,16 +688,11 @@ function buildAnthropicProxyRequest({
// resolves the right credentials and Anthropic URL.
const headers: Record<string, string> = {
'X-Provider': modelProvider.provider,
'anthropic-version': '2023-06-01'
'anthropic-version': '2023-06-01',
...testedCredentialHeaders({ apiKey, resourcePath, resourceValue })
}
if (resourcePath) {
headers['X-Resource-Path'] = resourcePath
} else if (apiKey) {
headers['X-API-Key'] = apiKey
}
const client = apiKey
const client = usesGlobalAiProxy({ apiKey, resourcePath, resourceValue })
? createAnthropicProxyClient(getAiProxyBaseURL())
: workspace
? workspaceAIClients.createAnthropicClient(workspace)
@@ -959,6 +987,7 @@ export async function getNonStreamingCompletion(
options?: {
apiKey?: string // testing API KEY using the global ai proxy
resourcePath?: string // testing resource path passed as a header to the backend proxy
resourceValue?: Record<string, any> // testing an unsaved resource value, same route
workspace?: string // use a specific workspace proxy when testing a workspace resource
forceModelProvider?: AIProviderModel
maxTokensCap?: number // hard ceiling on output tokens (see METADATA_MAX_TOKENS)
@@ -974,6 +1003,7 @@ export async function getNonStreamingCompletion(
apiKey: options?.apiKey,
workspace: options?.workspace,
resourcePath: options?.resourcePath,
resourceValue: options?.resourceValue,
maxTokensCap: options?.maxTokensCap
})
}
@@ -1009,22 +1039,11 @@ export async function getNonStreamingCompletion(
'X-Provider': provider
}
}
if (options?.resourcePath) {
fetchOptions.headers = {
...fetchOptions.headers,
'X-Resource-Path': options.resourcePath
}
} else if (options?.apiKey) {
if (provider === 'customai') {
throw new Error('Cannot test API key for Custom AI, only resource path is supported')
}
fetchOptions.headers = {
...fetchOptions.headers,
'X-API-Key': options.apiKey
}
if (usesGlobalAiProxy(options ?? {}) && provider === 'customai') {
throw new Error('Cannot test API key for Custom AI, only resource path is supported')
}
const openaiClient = options?.apiKey
fetchOptions.headers = { ...fetchOptions.headers, ...testedCredentialHeaders(options ?? {}) }
const openaiClient = usesGlobalAiProxy(options ?? {})
? createOpenAIProxyClient(getAiProxyBaseURL())
: options?.workspace
? workspaceAIClients.createOpenaiClient(options.workspace)
@@ -3,7 +3,6 @@
import { type FlowModule } from '$lib/gen'
import { createEventDispatcher, getContext } from 'svelte'
import { Pen, RefreshCcw, Save } from 'lucide-svelte'
import DropdownV2 from '../../DropdownV2.svelte'
import type { FlowEditorContext } from '../types'
import { sendUserToast } from '$lib/utils'
import type { FlowBuilderWhitelabelCustomUi } from '$lib/components/custom_ui'
@@ -83,16 +82,13 @@
tag={module.value.tag}
on:change={(e) => dispatch('tagChange', e.detail)}
/>
<DropdownV2
size="sm"
placement="bottom-end"
items={[
{
displayName: 'Save to workspace',
icon: Save,
action: () => dispatch('createScriptFromInlineScript')
}
]}
<Button
unifiedSize="sm"
variant="subtle"
startIcon={{ icon: Save }}
on:click={() => dispatch('createScriptFromInlineScript')}
iconOnly={true}
title="Save to workspace"
/>
{/if}
</div>
+1 -1
View File
@@ -4,7 +4,7 @@ verify_ssl = true
name = "pypi"
[packages]
wmill = ">=1.820.0"
wmill = ">=1.821.0"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: '3.0.3'
info:
version: 1.820.0
version: 1.821.0
title: OpenFlow Spec
contact:
name: Ruben Fiszel
@@ -12,7 +12,7 @@
RootModule = 'WindmillClient.psm1'
# Version number of this module.
ModuleVersion = '1.820.0'
ModuleVersion = '1.821.0'
# Supported PSEditions
# CompatiblePSEditions = @()
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
version = "1.820.0"
version = "1.821.0"
description = "A client library for accessing Windmill server wrapping the Windmill client API"
license = "Apache-2.0"
homepage = "https://windmill.dev"
@@ -88,6 +88,9 @@ datatable related commands
- `-d --datatable <datatable:string>` - Target datatable (default: main)
- `datatable migrate down` - roll back the most recent migration on the main datatable (or one via --datatable)
- `-d --datatable <datatable:string>` - Target datatable (default: main)
- `datatable migrate status` - show applied and pending migrations on the main datatable (or one via --datatable)
- `-d --datatable <datatable:string>` - Target datatable (default: main)
- `--json` - Output as JSON (for piping to jq)
- `datatable create [name:string]` - register a datatable database in the workspace (default: instance-backed 'main') so scripts can use datatable://<name>
- `--resource <resource:string>` - Back the datatable with an existing postgresql resource path instead of the instance database
- `--force` - Allow adding to a workspace that already has datatables (fork metadata on existing ones is not preserved)
+3
View File
@@ -3434,6 +3434,9 @@ datatable related commands
- \`-d --datatable <datatable:string>\` - Target datatable (default: main)
- \`datatable migrate down\` - roll back the most recent migration on the main datatable (or one via --datatable)
- \`-d --datatable <datatable:string>\` - Target datatable (default: main)
- \`datatable migrate status\` - show applied and pending migrations on the main datatable (or one via --datatable)
- \`-d --datatable <datatable:string>\` - Target datatable (default: main)
- \`--json\` - Output as JSON (for piping to jq)
- \`datatable create [name:string]\` - register a datatable database in the workspace (default: instance-backed 'main') so scripts can use datatable://<name>
- \`--resource <resource:string>\` - Back the datatable with an existing postgresql resource path instead of the instance database
- \`--force\` - Allow adding to a workspace that already has datatables (fork metadata on existing ones is not preserved)
@@ -93,6 +93,9 @@ datatable related commands
- `-d --datatable <datatable:string>` - Target datatable (default: main)
- `datatable migrate down` - roll back the most recent migration on the main datatable (or one via --datatable)
- `-d --datatable <datatable:string>` - Target datatable (default: main)
- `datatable migrate status` - show applied and pending migrations on the main datatable (or one via --datatable)
- `-d --datatable <datatable:string>` - Target datatable (default: main)
- `--json` - Output as JSON (for piping to jq)
- `datatable create [name:string]` - register a datatable database in the workspace (default: instance-backed 'main') so scripts can use datatable://<name>
- `--resource <resource:string>` - Back the datatable with an existing postgresql resource path instead of the instance database
- `--force` - Allow adding to a workspace that already has datatables (fork metadata on existing ones is not preserved)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@windmill/windmill",
"version": "1.820.0",
"version": "1.821.0",
"exports": "./src/index.ts",
"publish": {
"exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts", "./wacError.ts"]
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "windmill-client",
"description": "Windmill SDK client for browsers and Node.js",
"version": "1.820.0",
"version": "1.821.0",
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"homepage": "https://github.com/windmill-labs/windmill/tree/main/typescript-client#readme",
+1 -1
View File
@@ -1 +1 @@
1.820.0
1.821.0
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-yaml-validator",
"version": "1.820.0",
"version": "1.821.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-yaml-validator",
"version": "1.820.0",
"version": "1.821.0",
"license": "Apache 2.0",
"dependencies": {
"@stoplight/yaml": "^4.3.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-yaml-validator",
"version": "1.820.0",
"version": "1.821.0",
"description": "YAML validator for Windmill flow, schedule, and trigger files",
"main": "dist/index.js",
"types": "dist/index.d.ts",