diff --git a/.github/workflows/ai-evals-scheduled.yml b/.github/workflows/ai-evals-scheduled.yml index 964e5d0cab..cc1c0d6763 100644 --- a/.github/workflows/ai-evals-scheduled.yml +++ b/.github/workflows/ai-evals-scheduled.yml @@ -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 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 78bc4dfc98..afbaa8e7b0 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.820.0" + ".": "1.821.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index d1a8762b7f..8aa66a785d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/Dockerfile b/Dockerfile index 0fec95b95a..8d229dba56 100644 --- a/Dockerfile +++ b/Dockerfile @@ -318,7 +318,7 @@ COPY --from=nsjail /nsjail/nsjail /bin/nsjail # crane: pulls + flattens images for the sandboxed container runtime (`# sandbox `). # 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" \ diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 114fb76b67..abc735f0c7 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -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", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 9c09265dbd..d2bec22817 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -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 "] edition = "2021" diff --git a/backend/oauth_connect.json b/backend/oauth_connect.json index 0509e7f30a..fde54c39e2 100644 --- a/backend/oauth_connect.json +++ b/backend/oauth_connect.json @@ -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" } ], diff --git a/backend/parsers/windmill-parser-wasm/Cargo.lock b/backend/parsers/windmill-parser-wasm/Cargo.lock index 40dcff21f8..62c0cc5a8d 100644 --- a/backend/parsers/windmill-parser-wasm/Cargo.lock +++ b/backend/parsers/windmill-parser-wasm/Cargo.lock @@ -6216,7 +6216,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windmill-common" -version = "1.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", diff --git a/backend/parsers/windmill-parser-wasm/Cargo.toml b/backend/parsers/windmill-parser-wasm/Cargo.toml index ce27a75bc0..387a98aa1f 100644 --- a/backend/parsers/windmill-parser-wasm/Cargo.toml +++ b/backend/parsers/windmill-parser-wasm/Cargo.toml @@ -12,7 +12,7 @@ resolver = "2" members = ["."] [workspace.package] -version = "1.820.0" +version = "1.821.0" edition = "2021" authors = ["Ruben Fiszel "] diff --git a/backend/windmill-api-integration-tests/tests/ai_routes.rs b/backend/windmill-api-integration-tests/tests/ai_routes.rs index cfd93caef5..79152d82f7 100644 --- a/backend/windmill-api-integration-tests/tests/ai_routes.rs +++ b/backend/windmill-api-integration-tests/tests/ai_routes.rs @@ -232,3 +232,77 @@ async fn test_ai_proxy_x_resource_path_enforces_rls(db: Pool) -> 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) -> 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(()) +} diff --git a/backend/windmill-api-npm-proxy/src/lib.rs b/backend/windmill-api-npm-proxy/src/lib.rs index f018ad0258..48759a6ded 100644 --- a/backend/windmill-api-npm-proxy/src/lib.rs +++ b/backend/windmill-api-npm-proxy/src/lib.rs @@ -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, + auth: &Option, registry_base_url: &str, ) -> Result { 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, package: &str, -) -> Result<(Arc, String, Option)> { - let (registry_url, auth_token) = get_npm_registry(db) +) -> Result<(Arc, String, Option)> { + 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(®istry_url, &auth_token, package).await?; - Ok((document, registry_url, auth_token)) + let document = fetch_package_json_from(®istry_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, + auth: &Option, package: &str, ) -> Result> { 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, ®istry_url)? + let response = build_registry_request(&package_url, auth, ®istry_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, + auth: &Option, ) -> Result { 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, + auth: &Option, ) -> Result { - 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, ®istry_url, &auth_token) + fetch_tarball(&package_json, &package, &version, ®istry_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>, ) -> Result { 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, ®istry_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, -) -> Result)>> { +) -> Result)>> { 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 { // 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(®istry_url, &auth_token, package).await?; + let package_json = fetch_package_json_from(®istry_url, &auth, package).await?; let archive = - fetch_tarball(&package_json, package, version, ®istry_url, &auth_token).await?; + fetch_tarball(&package_json, package, version, ®istry_url, &auth).await?; let target = dir.clone(); let extracted = blocking(move || { diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index d8755f8995..d9aeb63707 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.820.0 + version: 1.821.0 title: Windmill API contact: diff --git a/backend/windmill-api/src/ai.rs b/backend/windmill-api/src/ai.rs index 3255b629da..855cd29987 100644 --- a/backend/windmill-api/src/ai.rs +++ b/backend/windmill-api/src/ai.rs @@ -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, @@ -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 = 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::(&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::(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() => diff --git a/backend/windmill-common/src/utils.rs b/backend/windmill-common/src/utils.rs index b247c66532..af6bdfe2b3 100644 --- a/backend/windmill-common/src/utils.rs +++ b/backend/windmill-common/src/utils.rs @@ -2206,11 +2206,30 @@ mod tests { } } -/// Parse .npmrc content to extract the default registry URL and its auth token. -/// Returns `Some((registry_url, Option))` if a default registry is found. -pub fn parse_npmrc_registry(npmrc_content: &str) -> Option<(String, Option)> { +/// 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))` 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)> { + use base64::{ + alphabet, + engine::{ + general_purpose::STANDARD, DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig, + }, + Engine, + }; + let mut registry_url: Option = 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 Option<(String, Option 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); diff --git a/backend/windmill-oauth/src/lib.rs b/backend/windmill-oauth/src/lib.rs index d37ab44742..15f5ac6b9e 100644 --- a/backend/windmill-oauth/src/lib.rs +++ b/backend/windmill-oauth/src/lib.rs @@ -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>, /// Default scopes for the client-credentials (2-legged) flow. These differ /// from the authorization-code `scopes` for most providers (member/consent diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index d81cd2891c..1ef3f82939 100644 --- a/benchmarks/lib.ts +++ b/benchmarks/lib.ts @@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts"; import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts"; import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts"; -export const VERSION = "v1.820.0"; +export const VERSION = "v1.821.0"; export async function login(email: string, password: string): Promise { return await windmill.UserService.login({ diff --git a/chat-sdk/package-lock.json b/chat-sdk/package-lock.json index 652d75acac..9d41c6cbfc 100644 --- a/chat-sdk/package-lock.json +++ b/chat-sdk/package-lock.json @@ -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", diff --git a/chat-sdk/package.json b/chat-sdk/package.json index eb57f7e969..6a189eb31b 100644 --- a/chat-sdk/package.json +++ b/chat-sdk/package.json @@ -1,7 +1,7 @@ { "name": "windmill-chat", "description": "Build chat interfaces on Windmill flows deployed in chat mode, from any frontend or raw app", - "version": "1.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", diff --git a/cli/src/commands/datatable/datatable.ts b/cli/src/commands/datatable/datatable.ts index 46ee7b4c39..3c0c5d6c65 100644 --- a/cli/src/commands/datatable/datatable.ts +++ b/cli/src/commands/datatable/datatable.ts @@ -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 = { + 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 ", "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 ", + "Target datatable (default: main)", + ) + .option("--json", "Output as JSON (for piping to jq)") + .action(migrateStatus as any); type DataTableResourceType = "postgresql" | "instance" | "external_instance"; diff --git a/cli/src/commands/datatable_migrations.ts b/cli/src/commands/datatable_migrations.ts index 4fcaffd4fa..a637f99968 100644 --- a/cli/src/commands/datatable_migrations.ts +++ b/cli/src/commands/datatable_migrations.ts @@ -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 { 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 }); diff --git a/cli/src/core/constants.ts b/cli/src/core/constants.ts index 3246083091..961a4474bd 100644 --- a/cli/src/core/constants.ts +++ b/cli/src/core/constants.ts @@ -10,4 +10,4 @@ export const WM_FORK_PREFIX = "wm-fork"; // (e.g. utils.ts) can read it without importing main.ts and creating a circular // dependency (main → workspace → utils → main) that triggers a TDZ. // Re-exported from main.ts for backwards compatibility. -export const VERSION = "1.820.0"; +export const VERSION = "1.821.0"; diff --git a/cli/src/guidance/skills.gen.ts b/cli/src/guidance/skills.gen.ts index 808761731e..8e87ed723e 100644 --- a/cli/src/guidance/skills.gen.ts +++ b/cli/src/guidance/skills.gen.ts @@ -7916,6 +7916,9 @@ datatable related commands - \`-d --datatable \` - Target datatable (default: main) - \`datatable migrate down\` - roll back the most recent migration on the main datatable (or one via --datatable) - \`-d --datatable \` - Target datatable (default: main) + - \`datatable migrate status\` - show applied and pending migrations on the main datatable (or one via --datatable) + - \`-d --datatable \` - 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:// - \`--resource \` - 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) diff --git a/docker/DockerfileSlim b/docker/DockerfileSlim index 6418a2cfda..45986bbd5f 100644 --- a/docker/DockerfileSlim +++ b/docker/DockerfileSlim @@ -96,7 +96,7 @@ COPY --from=nsjail /nsjail/nsjail /bin/nsjail # crane: pulls + flattens images for the sandboxed container runtime (`# sandbox `). # 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" \ diff --git a/docker/DockerfileSlimEe b/docker/DockerfileSlimEe index 24ef941b87..5ac24f5c1d 100644 --- a/docker/DockerfileSlimEe +++ b/docker/DockerfileSlimEe @@ -96,7 +96,7 @@ COPY --from=nsjail /nsjail/nsjail /bin/nsjail # crane: pulls + flattens images for the sandboxed container runtime (`# sandbox `). # 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" \ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4da3930df8..12227052a2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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": { diff --git a/frontend/package.json b/frontend/package.json index 3efd1155ef..13602b01f9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/lib/components/ApiConnectForm.svelte b/frontend/src/lib/components/ApiConnectForm.svelte index 2e4e4b526c..37342912c7 100644 --- a/frontend/src/lib/components/ApiConnectForm.svelte +++ b/frontend/src/lib/components/ApiConnectForm.svelte @@ -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} /> + {#if aiProvider} + m.provider === aiProvider)?.model} + /> + {/if} {#if resourceType == 'postgresql'} {/if} + {#if aiProvider} + m.provider === aiProvider)?.model} + /> + {/if} {#if resource_type === 'git_repository' && ws && (actingUser?.is_admin || actingUser?.is_super_admin)} | 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) - + }} + >Test key + + diff --git a/frontend/src/lib/components/copilot/aiResourceProvider.ts b/frontend/src/lib/components/copilot/aiResourceProvider.ts new file mode 100644 index 0000000000..91ae926dfc --- /dev/null +++ b/frontend/src/lib/components/copilot/aiResourceProvider.ts @@ -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 +} diff --git a/frontend/src/lib/components/copilot/chat/openai-responses.ts b/frontend/src/lib/components/copilot/chat/openai-responses.ts index 31d3b379ac..ebaa5b1c38 100644 --- a/frontend/src/lib/components/copilot/chat/openai-responses.ts +++ b/frontend/src/lib/components/copilot/chat/openai-responses.ts @@ -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 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) diff --git a/frontend/src/lib/components/copilot/lib.ts b/frontend/src/lib/components/copilot/lib.ts index ba718bf760..40f1dee6a6 100644 --- a/frontend/src/lib/components/copilot/lib.ts +++ b/frontend/src/lib/components/copilot/lib.ts @@ -581,10 +581,39 @@ class WorkspacedAIClients { export const workspaceAIClients = new WorkspacedAIClients() +interface TestedCredential { + apiKey?: string + resourcePath?: string + resourceValue?: Record +} + +/** The header naming the credential under test; a resource wins over a bare key. */ +export function testedCredentialHeaders({ + apiKey, + resourcePath, + resourceValue +}: TestedCredential): Record { + 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 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 maxTokensCap?: number } @@ -647,6 +679,7 @@ function buildAnthropicProxyRequest({ apiKey, workspace, resourcePath, + resourceValue, maxTokensCap }: Omit) { const { system, messages: anthropicMessages } = convertOpenAIToAnthropicMessages(messages) @@ -655,16 +688,11 @@ function buildAnthropicProxyRequest({ // resolves the right credentials and Anthropic URL. const headers: Record = { '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 // 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) diff --git a/frontend/src/lib/components/flows/content/FlowModuleHeader.svelte b/frontend/src/lib/components/flows/content/FlowModuleHeader.svelte index 63d053fe14..a1e957a045 100644 --- a/frontend/src/lib/components/flows/content/FlowModuleHeader.svelte +++ b/frontend/src/lib/components/flows/content/FlowModuleHeader.svelte @@ -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)} /> - dispatch('createScriptFromInlineScript') - } - ]} +