mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
Merge remote-tracking branch 'origin/main' into glm/improve-multiselect
This commit is contained in:
@@ -12,6 +12,7 @@ sed -i '' -e "/^export const VERSION =/s/= .*/= \"v$VERSION\";/" ${root_dirpath}
|
||||
sed -i '' -e "/version: /s/: .*/: $VERSION/" ${root_dirpath}/backend/windmill-api/openapi.yaml
|
||||
sed -i '' -e "/version: /s/: .*/: $VERSION/" ${root_dirpath}/openflow.openapi.yaml
|
||||
sed -i '' -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/typescript-client/package.json
|
||||
sed -i '' -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/chat-sdk/package.json
|
||||
sed -i '' -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/frontend/package.json
|
||||
sed -i '' -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
|
||||
sed -i '' -e "/^windmill-api =/s/= .*/= \"\\^$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
|
||||
|
||||
@@ -13,6 +13,7 @@ sed -i -e "/version: /s/: .*/: $VERSION/" ${root_dirpath}/backend/windmill-api/o
|
||||
sed -i -e "/version: /s/: .*/: $VERSION/" ${root_dirpath}/openflow.openapi.yaml
|
||||
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/typescript-client/package.json
|
||||
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/typescript-client/jsr.json
|
||||
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/chat-sdk/package.json
|
||||
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/frontend/package.json
|
||||
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/windmill-yaml-validator/package.json
|
||||
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
|
||||
@@ -33,3 +34,5 @@ cd ${root_dirpath}/frontend && npm i --package-lock-only --ignore-scripts
|
||||
# The CLI installs this package on every `bun install`, which would otherwise rewrite the
|
||||
# lockfile's version and leave a dirty tree.
|
||||
cd ${root_dirpath}/windmill-yaml-validator && npm i --package-lock-only --ignore-scripts
|
||||
|
||||
cd ${root_dirpath}/chat-sdk && npm i --package-lock-only --ignore-scripts
|
||||
|
||||
@@ -9,6 +9,8 @@ on:
|
||||
push:
|
||||
paths:
|
||||
- "frontend/**"
|
||||
# The flow chat compiles the chat SDK's source in (svelte.config.js alias).
|
||||
- "chat-sdk/src/**"
|
||||
- ".github/workflows/frontend-check.yml"
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -17,6 +17,17 @@ jobs:
|
||||
- run: cd typescript-client && ./publish.sh --access public && cd ..
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
publish_chat_sdk:
|
||||
runs-on: ubicloud-standard-8
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "20.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- run: cd chat-sdk && npm ci && npm run build && npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
publish_cli:
|
||||
runs-on: ubicloud-standard-8
|
||||
steps:
|
||||
|
||||
@@ -32,6 +32,25 @@ jobs:
|
||||
working-directory: ./typescript-client
|
||||
run: bun test --timeout 120000 tests/
|
||||
|
||||
chat-sdk:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.x"
|
||||
|
||||
- name: Run tests
|
||||
working-directory: ./chat-sdk
|
||||
run: npm ci && npm run check && bun test
|
||||
|
||||
python-client:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "1.811.1"
|
||||
".": "1.813.0"
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ Open-source platform for internal tools, workflows, API integrations, background
|
||||
reaches the DB only through the API, so `Connection::Http` paths are never taken by a plain
|
||||
`cargo run`; a normal build cannot start one at all.
|
||||
- **Enterprise**: `docs/enterprise.md` — EE file conventions and PR workflow
|
||||
- **Auth surface**: `docs/auth-surface.md` — credential precedence, session/cache invalidation
|
||||
scope, how OAuth login matches `login_type`, and that every superadmin route refuses `$WM_TOKEN`.
|
||||
Read before designing anything that creates users, tokens or sessions.
|
||||
- **Product telemetry**: `docs/feature-telemetry.md` — when to instrument a new feature with
|
||||
`feature_usage`, and the four-step recipe. An unregistered `(feature, kind)` pair is dropped
|
||||
silently, so frontend-only instrumentation records nothing.
|
||||
|
||||
@@ -1,5 +1,61 @@
|
||||
# Changelog
|
||||
|
||||
## [1.813.0](https://github.com/windmill-labs/windmill/compare/v1.812.0...v1.813.0) (2026-09-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* back AI sessions up to the workspace object storage ([#11116](https://github.com/windmill-labs/windmill/issues/11116)) ([796b6e5](https://github.com/windmill-labs/windmill/commit/796b6e5297d8cceb842ec097f33ec1c3115058bd))
|
||||
* delete a browser's copy of an AI session past its workspace retention ([#11156](https://github.com/windmill-labs/windmill/issues/11156)) ([a48ae65](https://github.com/windmill-labs/windmill/commit/a48ae656ae59d600311f81ef357d08df5226515a))
|
||||
* rename saved agents from the agent editor and flag broken links ([#11147](https://github.com/windmill-labs/windmill/issues/11147)) ([57a99f6](https://github.com/windmill-labs/windmill/commit/57a99f66a88f195cac8f583b59d69d627cc1ec1d))
|
||||
* retention for AI sessions on the object store and in the browser ([#11152](https://github.com/windmill-labs/windmill/issues/11152)) ([ee6d317](https://github.com/windmill-labs/windmill/commit/ee6d317e318fa8a1506fb18d51c45b627070627a))
|
||||
* return an ai agent step's thinking in its job result ([#11140](https://github.com/windmill-labs/windmill/issues/11140)) ([c4e878e](https://github.com/windmill-labs/windmill/commit/c4e878e8313a72a16bfbe81fbb3935ee7728ec6f))
|
||||
* stream reasoning summaries in AI agent Responses API steps ([#11124](https://github.com/windmill-labs/windmill/issues/11124)) ([b51c0ea](https://github.com/windmill-labs/windmill/commit/b51c0eabbe774c78a9cbf824a3df6528d970b8f6))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **apps:** re-check access in place after a password sign-in ([#11166](https://github.com/windmill-labs/windmill/issues/11166)) ([49d0310](https://github.com/windmill-labs/windmill/commit/49d0310ecc08040b6c6fa4f402584543d679e2e8))
|
||||
* **apps:** run-mode inline app component uses only pinned content ([#11135](https://github.com/windmill-labs/windmill/issues/11135)) ([781b5a5](https://github.com/windmill-labs/windmill/commit/781b5a57e81eb721d97d7b87e23dd84f23895400))
|
||||
* **cli:** keep the workspace color when settings are synced from git ([#11144](https://github.com/windmill-labs/windmill/issues/11144)) ([129c045](https://github.com/windmill-labs/windmill/commit/129c04559548cd1bcf67758ec416fb2a48e7b928))
|
||||
* **cli:** resolve lockgen imports through modules a push leaves alone ([#11160](https://github.com/windmill-labs/windmill/issues/11160)) ([54553b2](https://github.com/windmill-labs/windmill/commit/54553b2add6941395f03ca34ee24cf335a3b23c2))
|
||||
* dispatch workflow-as-code tasks from a deployed flow's inline step ([#11146](https://github.com/windmill-labs/windmill/issues/11146)) ([e8078f2](https://github.com/windmill-labs/windmill/commit/e8078f2a963166b09849650424583f5dcfd28a84))
|
||||
* keep sidebar confirmation dialogs from being confined to the rail ([#11158](https://github.com/windmill-labs/windmill/issues/11158)) ([b9b5988](https://github.com/windmill-labs/windmill/commit/b9b5988ebdf3edd75add445282977c516ef5ed51))
|
||||
* keep the instance users table's actions and header in view ([#11145](https://github.com/windmill-labs/windmill/issues/11145)) ([a9a9335](https://github.com/windmill-labs/windmill/commit/a9a9335a34a13ffd8cd2699adc92087b679548ca))
|
||||
* stop reading an array job result as wm_failure or http response ([#11154](https://github.com/windmill-labs/windmill/issues/11154)) ([9a8a9c4](https://github.com/windmill-labs/windmill/commit/9a8a9c480cf008761ec6ceaff694085a252a32ae))
|
||||
* walk the whole fork ancestry for app installations and fork conflicts ([#11151](https://github.com/windmill-labs/windmill/issues/11151)) ([73dc892](https://github.com/windmill-labs/windmill/commit/73dc892f9c9c840a5f0fb12fcbb28bbe0f38795f))
|
||||
* **worker:** bound cache transfers and import fetches in bun jobs ([#11138](https://github.com/windmill-labs/windmill/issues/11138)) ([31c4325](https://github.com/windmill-labs/windmill/commit/31c43255fdcc827c3fdf65e40238f8f3a83201cd))
|
||||
|
||||
## [1.812.0](https://github.com/windmill-labs/windmill/compare/v1.811.1...v1.812.0) (2026-09-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add per-route CORS origin allowlist for HTTP triggers ([#10833](https://github.com/windmill-labs/windmill/issues/10833)) ([d8d7332](https://github.com/windmill-labs/windmill/commit/d8d7332eb6d92f7a55b82890de5de4196039b40d))
|
||||
* **ai-sessions:** share session artifacts with the workspace by link ([#11115](https://github.com/windmill-labs/windmill/issues/11115)) ([57a134e](https://github.com/windmill-labs/windmill/commit/57a134e2de18e27b3c1d3066a60b892af1089b30))
|
||||
* **cli:** list, get and restore trashed items with wmill trash ([#11125](https://github.com/windmill-labs/windmill/issues/11125)) ([a95e950](https://github.com/windmill-labs/windmill/commit/a95e950529f6f01a220e09d322d5a4fb507ea694))
|
||||
* dynamic AI agent toolsets ([#11050](https://github.com/windmill-labs/windmill/issues/11050)) ([a78beff](https://github.com/windmill-labs/windmill/commit/a78beff743f6bb289805c263a8d32515bea9688f))
|
||||
* **git-sync:** gate GitHub PRs on Windmill CI test results (WIN-2051) ([#10096](https://github.com/windmill-labs/windmill/issues/10096)) ([80eba80](https://github.com/windmill-labs/windmill/commit/80eba80d6ed51753cfaa67310f1a0f5dd5ce0484))
|
||||
* pre-approved cloud accounts: login links, OAuth adoption, setup, and the trial bridge ([#10875](https://github.com/windmill-labs/windmill/issues/10875)) ([91e6dc3](https://github.com/windmill-labs/windmill/commit/91e6dc39ce795fafc2bed0d799b62c9880fd6430))
|
||||
* run a flow step test through the chat's argument form ([#11114](https://github.com/windmill-labs/windmill/issues/11114)) ([5d32b61](https://github.com/windmill-labs/windmill/commit/5d32b6106788b665e4752fcac63ff1ef457d604e))
|
||||
* store resource type display names and label hub integrations ([#11113](https://github.com/windmill-labs/windmill/issues/11113)) ([42f4896](https://github.com/windmill-labs/windmill/commit/42f489685bc87a8479818175c87b5a21b0c17998))
|
||||
* windmill-chat sdk for chat-mode flows in external frontends and raw apps ([#11117](https://github.com/windmill-labs/windmill/issues/11117)) ([e8c02c0](https://github.com/windmill-labs/windmill/commit/e8c02c04cdb1a3f199f3f0a8b53a839a53d4a1d9))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ai-chat:** hide other users' MCP servers from the chat unless shared ([#11112](https://github.com/windmill-labs/windmill/issues/11112)) ([244ec13](https://github.com/windmill-labs/windmill/commit/244ec132914a6e689d7d79da9cf2cb39f920aaef))
|
||||
* **cli:** say where a sync push deleted variable or resource went ([#10851](https://github.com/windmill-labs/windmill/issues/10851)) ([d54a66f](https://github.com/windmill-labs/windmill/commit/d54a66f15c09c34f7a2b45c2e6e9649807a19265))
|
||||
* **cli:** stage a rewritten shared lockfile on git-sync deploy push ([#11126](https://github.com/windmill-labs/windmill/issues/11126)) ([75ee497](https://github.com/windmill-labs/windmill/commit/75ee497011dca076de0923ded9da8e23e08bfb84))
|
||||
* **flows:** stop re-evaluating skip_if once a loop is in progress ([#11008](https://github.com/windmill-labs/windmill/issues/11008)) ([56e21bc](https://github.com/windmill-labs/windmill/commit/56e21bce832182528562688acd13ec416e01ebfc))
|
||||
* **git-sync:** run auto-pull as the admin who enabled it ([#11121](https://github.com/windmill-labs/windmill/issues/11121)) ([69e6efd](https://github.com/windmill-labs/windmill/commit/69e6efd875e020779ea115c096331da8eae2ffe7))
|
||||
* keep a script draft's password marking through the chat's run form ([#11110](https://github.com/windmill-labs/windmill/issues/11110)) ([56dd940](https://github.com/windmill-labs/windmill/commit/56dd940e34b146c3ca25de7959b4fb618308eb86))
|
||||
* **python:** parse wheel RECORD paths as RFC 4180 csv fields ([#11133](https://github.com/windmill-labs/windmill/issues/11133)) ([9696308](https://github.com/windmill-labs/windmill/commit/96963080f1711192fe1d9bc4142c1710511504d4))
|
||||
* re-attach flow chat to the same job on SSE timeout instead of re-running it ([#11122](https://github.com/windmill-labs/windmill/issues/11122)) ([94c548c](https://github.com/windmill-labs/windmill/commit/94c548cd5dc43131e0471b0edabe496dff245862))
|
||||
* set the enclosing span's trace context on exported log records ([#11123](https://github.com/windmill-labs/windmill/issues/11123)) ([d0cac08](https://github.com/windmill-labs/windmill/commit/d0cac0807f1b6f4fc76d6e5f7e27e03d30abec8d))
|
||||
* skip instance group members that are not email addresses ([#11128](https://github.com/windmill-labs/windmill/issues/11128)) ([e3e638f](https://github.com/windmill-labs/windmill/commit/e3e638f7f587f0ee090d06436575b65a0860a855))
|
||||
* wake a WAC parent from every path that completes its child ([#11119](https://github.com/windmill-labs/windmill/issues/11119)) ([0b1e9c0](https://github.com/windmill-labs/windmill/commit/0b1e9c0dda2ae55c56b1e0de5c0419b4511b973f))
|
||||
|
||||
## [1.811.1](https://github.com/windmill-labs/windmill/compare/v1.811.0...v1.811.1) (2026-09-13)
|
||||
|
||||
|
||||
|
||||
@@ -73,6 +73,8 @@ COPY /backend/oauth_connect.json /backend/oauth_connect.json
|
||||
COPY /openflow.openapi.yaml /openflow.openapi.yaml
|
||||
COPY /backend/windmill-api/build_openapi.sh /backend/windmill-api/build_openapi.sh
|
||||
COPY /system_prompts/auto-generated /system_prompts/auto-generated
|
||||
# The flow chat imports the chat SDK's source (svelte.config.js alias `windmill-chat`).
|
||||
COPY /chat-sdk/src /chat-sdk/src
|
||||
|
||||
RUN cd /backend/windmill-api && . ./build_openapi.sh
|
||||
COPY /backend/parsers/windmill-parser-wasm/pkg/ /backend/parsers/windmill-parser-wasm/pkg/
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO ai_shared_artifact\n (workspace_id, artifact_id, email, created_by, name, kind, version, content)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ON CONFLICT (workspace_id, email, artifact_id) DO UPDATE\n SET created_by = EXCLUDED.created_by,\n name = EXCLUDED.name,\n kind = EXCLUDED.kind,\n version = EXCLUDED.version,\n content = EXCLUDED.content,\n shared_at = now()\n RETURNING id, shared_at, (xmax = 0) AS \"inserted!\"",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "shared_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "inserted!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Int4",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "0589cb0f96e17ecadae4923be70a6cf0148a9e10c1c4ad0f99312b8e99ec1b8a"
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO login_link (token_hash, email, rd, expiration, created_by)\n VALUES ($1, $2, $3, $4, $5)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bpchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Timestamptz",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "071de805623be166dddd2655f099bed4ebbf6a03ec5988acf072c83818d57a02"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT f.path\n FROM workspace_runnable_dependencies wru \n JOIN flow f\n ON wru.flow_path = f.path AND wru.workspace_id = f.workspace_id\n WHERE wru.runnable_path = $1 AND wru.runnable_is_flow = $2 AND wru.workspace_id = $3",
|
||||
"query": "SELECT f.path\n FROM workspace_runnable_dependencies wru \n JOIN flow f\n ON wru.flow_path = f.path AND wru.workspace_id = f.workspace_id\n WHERE wru.runnable_path = $1 AND wru.runnable_is_flow = $2 AND NOT wru.runnable_is_agent AND wru.workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -20,5 +20,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "00c0ae12b19ba495f307f0ce6b4833947c5b3fe45826fc5468e326d171d95236"
|
||||
"hash": "0c49b098051900b834cb791e37af2e38967680a77316ee9a53e67d70d7df9f63"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT username, email FROM usr WHERE workspace_id = $1 AND is_admin = true AND operator = false AND disabled = false ORDER BY username LIMIT 1",
|
||||
"query": "SELECT u.username, u.email FROM usr u WHERE u.workspace_id = $1 AND u.is_admin AND NOT u.operator AND NOT u.disabled AND NOT EXISTS (SELECT 1 FROM password p WHERE p.email = u.email AND p.disabled) ORDER BY u.username LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -24,5 +24,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3202bed875693ae923f496272cd8ad89b2f17a9d3ef4659c2d2284415177b32c"
|
||||
"hash": "0c4dc0e9dc159fac7e41492c78a4e4e0b12b105d4475d7eba2d3a9573b93e388"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT DISTINCT f.path\n FROM workspace_runnable_dependencies wru \n JOIN flow f\n ON wru.flow_path = f.path AND wru.workspace_id = f.workspace_id\n WHERE wru.runnable_path LIKE $1 || '%' AND wru.runnable_is_flow = $2 AND wru.workspace_id = $3",
|
||||
"query": "SELECT DISTINCT f.path\n FROM workspace_runnable_dependencies wru \n JOIN flow f\n ON wru.flow_path = f.path AND wru.workspace_id = f.workspace_id\n WHERE wru.runnable_path LIKE $1 || '%' AND wru.runnable_is_flow = $2 AND NOT wru.runnable_is_agent AND wru.workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -20,5 +20,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "dc5eeb7b7bf0b7217ef66eb950ab7e9cf578bba7bd1eec981526be4067bcb314"
|
||||
"hash": "139e153d1ebe584e878d3b2569551892fa3899934e7e0e51ad23bfcd2d6d3d08"
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COALESCE(username, split_part(email, '@', 1)) AS \"username!\", email FROM password WHERE super_admin = true AND disabled = false ORDER BY email LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "username!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "17cdf02b4912078459526205849246fd2bdf9e3fce89852120aa4ad4ad6abfc3"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO resource_type (workspace_id, name, schema, description, format_extension, display_name, edited_at)\n VALUES ('admins', $1, $2, $3, $4, $6, now())\n ON CONFLICT (workspace_id, name) DO UPDATE\n SET schema = EXCLUDED.schema, description = EXCLUDED.description,\n -- A fileset is a set of files, so it cannot also be one file.\n -- Create and update reject the pair; this writer bypasses both, so\n -- it declines the extension rather than persisting the forbidden\n -- combination onto a same-named local fileset.\n format_extension = CASE\n WHEN resource_type.is_fileset THEN NULL\n WHEN $5 THEN EXCLUDED.format_extension\n ELSE resource_type.format_extension END,\n display_name = CASE WHEN $7 THEN EXCLUDED.display_name ELSE resource_type.display_name END,\n edited_at = now()",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "212bf5b32de102a9c537907aaff04befc3a6596c258086805dd42f513ddb3ead"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE cloud_trial_offer SET consumed_at = now() WHERE email = $1 AND consumed_at IS NULL",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "25f27dba5c0ea81d9412bdf1986c2a38b2dcf3976fbd67522c4644ee9bddc330"
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE v2_job_completed SET\n workflow_as_code_status = jsonb_set(\n jsonb_set(\n workflow_as_code_status,\n array[$1],\n COALESCE(workflow_as_code_status->$1, '{}'::jsonb)\n ),\n array[$1, 'duration_ms'],\n to_jsonb($2::bigint)\n )\n WHERE id = $3 AND workflow_as_code_status IS NOT NULL",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "29935e89475f637d765c516f1aa2be2f0f31fb50d519b42a056d0d73417599a3"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO workspace_runnable_dependencies (flow_path, runnable_path, script_hash, runnable_is_flow, runnable_is_agent, workspace_id, app_path)\n SELECT flow_path, runnable_path, script_hash, runnable_is_flow, runnable_is_agent, $1, app_path\n FROM workspace_runnable_dependencies\n WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2aa87574b437f0e29991696564c4250441863ab47c345d9685f53c7e224b4887"
|
||||
}
|
||||
+12
-4
@@ -1,15 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO v2_job_completed\n (workspace_id, id, started_at, duration_ms, result, memory_peak, status, worker)\n SELECT q.workspace_id, q.id, q.started_at,\n COALESCE((EXTRACT('epoch' FROM now()) - EXTRACT('epoch' FROM COALESCE(q.started_at, now()))) * 1000, 0)::bigint,\n $2::jsonb, r.memory_peak, 'failure'::job_status, q.worker\n FROM v2_job_queue q\n LEFT JOIN v2_job_runtime r ON r.id = q.id\n WHERE q.id = $1\n ON CONFLICT (id) DO UPDATE SET status = 'failure', result = $2::jsonb",
|
||||
"query": "INSERT INTO v2_job_completed\n (workspace_id, id, started_at, duration_ms, result, memory_peak, status, worker)\n SELECT q.workspace_id, q.id, q.started_at,\n COALESCE((EXTRACT('epoch' FROM now()) - EXTRACT('epoch' FROM COALESCE(q.started_at, now()))) * 1000, 0)::bigint,\n $2::jsonb, r.memory_peak, 'failure'::job_status, q.worker\n FROM v2_job_queue q\n LEFT JOIN v2_job_runtime r ON r.id = q.id\n WHERE q.id = $1\n ON CONFLICT (id) DO UPDATE SET status = 'failure', result = $2::jsonb\n RETURNING duration_ms AS \"duration_ms!\"",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "duration_ms!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "beecb176df512e4a94771d0d73c4c597e07e53d499131b57e4d6441fd0af09cb"
|
||||
"hash": "2abc2a5830130b2b4b32983407abeea41923ba4122fa666c6e8d8b06dcb5a71f"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE password SET password_hash = $1, login_type = 'password'\n WHERE email = $2 AND login_type = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "310d91848c7a032846aa8be8c5e5f42477fc5ab17fac70864d1b2f7f91ac7f9d"
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT parent_job, flow_step_id FROM v2_job WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "parent_job",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "flow_step_id",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "32ca7941db013dacd2479962fa9ed5c8c64daec45ba820a6c8f7d7ab76cc40c9"
|
||||
}
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'admins')",
|
||||
"query": "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset, display_name from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'admins')",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -42,6 +42,11 @@
|
||||
"ordinal": 7,
|
||||
"name": "is_fileset",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "display_name",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -58,8 +63,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "623b061ccaa6bb883e95771fde8c911a165c9c430b7db389370361ca74d737f4"
|
||||
"hash": "36ddecbdad3cce7a2593171ff10a6b07cdb70994be5724f08f864d29517f4907"
|
||||
}
|
||||
+4
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM resource_type WHERE workspace_id = 'admins' AND name = $1 AND schema IS NOT DISTINCT FROM $2 AND description IS NOT DISTINCT FROM $3 AND ($5 IS NOT TRUE OR format_extension IS NOT DISTINCT FROM $4))",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM resource_type WHERE workspace_id = 'admins' AND name = $1 AND schema IS NOT DISTINCT FROM $2 AND description IS NOT DISTINCT FROM $3 AND ($5 IS NOT TRUE OR format_extension IS NOT DISTINCT FROM $4) AND ($7 IS NOT TRUE OR display_name IS NOT DISTINCT FROM $6))",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -15,6 +15,8 @@
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
@@ -22,5 +24,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "8ad79b80033b38ebddf6c8cd4d8cb160d41bac4c45a0fc74d9c9e96d3ef4486a"
|
||||
"hash": "386e14cf7572027f2c4ef313cd7cc5dd6c7b0da4f76131313d3e6d97f36c5c34"
|
||||
}
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset from resource_type WHERE (workspace_id = $1 OR workspace_id = 'admins') ORDER BY name",
|
||||
"query": "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset, display_name from resource_type WHERE (workspace_id = $1 OR workspace_id = 'admins') ORDER BY name",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -42,6 +42,11 @@
|
||||
"ordinal": 7,
|
||||
"name": "is_fileset",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "display_name",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -57,8 +62,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d0a95698b9a2c5e2543e94276d854d7e509c7db2c2ac7d395b7b53ad5dbc25e6"
|
||||
"hash": "38b6c6cb91d3ba38838a7a015c59ecdad014ce319066eabec0f0d7f53338698a"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT super_admin AS \"super_admin!\" FROM password WHERE email = $1 AND disabled = false",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "super_admin!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3fe41e2a72d02613a2b1c1c44fb0a7b681d101d286adfc6ff4548d1b9fdbab8c"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT profile FROM cloud_onboarding_profile WHERE email = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "profile",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "42783d94ee41c5b17ec16b480dd55af2a0ba2e827ae8add59d5e5465dc1d5743"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT DISTINCT f.path\n FROM workspace_runnable_dependencies wru\n JOIN flow f\n ON wru.flow_path = f.path AND wru.workspace_id = f.workspace_id\n WHERE wru.runnable_path = $1 AND wru.runnable_is_agent AND wru.workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "46e65196c2a4f07d171a22f1e45c6ac927bd0e6b0f383626e673225065ee90bf"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, company, username, first_time_user)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4ed69ae9e2a0d045ec63e327bc40c73aba9b34302e2f25cd6928a29d974bbb2c"
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT super_admin, devops FROM password WHERE email = $1 AND disabled = false FOR UPDATE",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "super_admin",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "devops",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5bd410d777a7a6d48129e9fee8402455082e0a172948be6441e5383552331c3f"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM cloud_trial_offer WHERE email = $1 AND consumed_at IS NULL)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "5ca0afc5a7b0437de221c8cc7b31e015ec604eb92a40b8649d054cabef1d8060"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO cloud_onboarding_profile (email, profile, created_by) VALUES ($1, $2, $3)\n ON CONFLICT (email) DO UPDATE SET profile = EXCLUDED.profile",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "64bc01a5d88680febabd794b6472b6d22720d71e555501011e1d6b3418064ed0"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM ai_shared_artifact\n WHERE shared_at <= now() - ($1::bigint::text || ' s')::interval",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6774bc0ec8ca8c6c48e8e111ab074b8c5beb1c3992f6413d7bcabc8159c0f9bb"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT r->'auto_pull'->>'enabled_by' AS \"enabled_by\"\n FROM workspace_settings, jsonb_array_elements(git_sync->'repositories') r\n WHERE workspace_id = $1 AND r->>'git_repo_resource_path' = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "enabled_by",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "6dc8032100a28c4a6e843370038dfec43dc8a042f9d95e63bf84b2dc15a72165"
|
||||
}
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset from resource_type ORDER BY name",
|
||||
"query": "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset, display_name from resource_type ORDER BY name",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -42,6 +42,11 @@
|
||||
"ordinal": 7,
|
||||
"name": "is_fileset",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "display_name",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -55,8 +60,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e253b9e7e6450652589d6ee7ffa86d600e449cd399ac781af8b40c1c444972c3"
|
||||
"hash": "6f993567336a2f5ff642ed54e3aaf4d070f803b6192739e9ebaad7427ce59251"
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE login_link SET consumed_at = now()\n WHERE token_hash = $1 AND consumed_at IS NULL AND expiration > now()\n RETURNING email, rd, created_by",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "rd",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "created_by",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bpchar"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "754598696e57a8c3ee6477d4f55f62e12019aa9582f06b9a47f7d04715eee24c"
|
||||
}
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset FROM resource_type WHERE workspace_id = $1",
|
||||
"query": "SELECT workspace_id, name, schema, description, created_by, edited_at, format_extension, is_fileset, display_name FROM resource_type WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -42,6 +42,11 @@
|
||||
"ordinal": 7,
|
||||
"name": "is_fileset",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "display_name",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -57,8 +62,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "45d5e9ead8193a04fd00c44a488590fdd2f7c4de45117a18360651655d153545"
|
||||
"hash": "82350027cf9722a993f27808e570e795ff9fb6b863dfe9c0bcad382c3b73a25b"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO resource_type (workspace_id, name, schema, description, edited_at, created_by, format_extension, is_fileset)\n SELECT $2, name, schema, description, edited_at, created_by, format_extension, is_fileset\n FROM resource_type\n WHERE workspace_id = $1",
|
||||
"query": "INSERT INTO resource_type (workspace_id, name, schema, description, edited_at, created_by, format_extension, is_fileset, display_name)\n SELECT $2, name, schema, description, edited_at, created_by, format_extension, is_fileset, display_name\n FROM resource_type\n WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -11,5 +11,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1c2157ce14e90f0751d7f0a9f2dbb3c5a5789a32423e75260098a5300a4af986"
|
||||
"hash": "86ad1e7ebe659f97877cc142c09676488dc6c80428e13e5db3c016e37072cfe4"
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT u.username, u.is_admin, u.operator, u.disabled, EXISTS (SELECT 1 FROM password p WHERE p.email = u.email AND p.disabled) AS \"instance_disabled!\" FROM usr u WHERE u.workspace_id = $1 AND u.email = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "is_admin",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "operator",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "disabled",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "instance_disabled!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "8bdfc02e7be54c2b610fed11cce75f8d2f1665d329b8adea424b3a6f1e5c7013"
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workspace_settings.workspace_id AS \"id!\",\n workspace_settings.ai_config->'sessions_retention_days' AS retention\n FROM workspace_settings\n LEFT JOIN usr ON usr.workspace_id = workspace_settings.workspace_id AND usr.email = $2\n WHERE workspace_settings.workspace_id = ANY($1)\n AND ($3 OR (usr.email IS NOT NULL AND NOT usr.disabled))",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "retention",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "8eee14066c86b4a4ef921576277c8a287bf1ff4b9d4301e3ea0efd8077936aff"
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, email, name, kind, version, created_by, content, shared_at\n FROM ai_shared_artifact\n WHERE workspace_id = $1 AND id = $2\n AND shared_at > now() - ($3::bigint::text || ' s')::interval",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "kind",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "version",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "created_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "content",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "shared_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Uuid",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "945230149990abda67fdf4779529207306fb19756ab9c3a3d7824a15542a5b42"
|
||||
}
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO resource_type (workspace_id, name, schema, description, format_extension, edited_at)\n VALUES ('admins', $1, $2, $3, $4, now())\n ON CONFLICT (workspace_id, name) DO UPDATE\n SET schema = EXCLUDED.schema, description = EXCLUDED.description,\n -- A fileset is a set of files, so it cannot also be one file.\n -- Create and update reject the pair; this writer bypasses both, so\n -- it declines the extension rather than persisting the forbidden\n -- combination onto a same-named local fileset.\n format_extension = CASE\n WHEN resource_type.is_fileset THEN NULL\n WHEN $5 THEN EXCLUDED.format_extension\n ELSE resource_type.format_extension END,\n edited_at = now()",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "972df41db505fbbd20a558b200a2e2e8bc43633707d8365f73130c5bca3923b9"
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT super_admin, devops, login_type FROM password WHERE email = $1 AND disabled = false",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "super_admin",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "devops",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "login_type",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a2be5aeb7e663b0fe403726b4a41a7760b21c7edb651452c80b46b54ec964901"
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO workspace_runnable_dependencies (flow_path, runnable_path, script_hash, runnable_is_flow, workspace_id, app_path)\n SELECT flow_path, runnable_path, script_hash, runnable_is_flow, $1, app_path\n FROM workspace_runnable_dependencies\n WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a54e2334c365f90577f68ebefc8f3bee9b93ba0387f566ad7f502cbee818296e"
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE v2_job_queue SET suspend = 0, suspend_until = NULL WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a72081cb042f09034338dcb49381e91093233cf16af0dae666b4743f3878b22e"
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO usr (workspace_id, username, email, is_admin, operator)\n SELECT $1::varchar, u.username, u.email, true, false FROM usr u\n WHERE u.workspace_id = $2 AND u.email = $3 AND u.is_admin AND NOT u.operator AND NOT u.disabled\n AND NOT EXISTS (SELECT 1 FROM password p WHERE p.email = u.email AND p.disabled)\n AND NOT EXISTS (SELECT 1 FROM usr f WHERE f.workspace_id = $1::varchar AND f.email = $3)\n ON CONFLICT DO NOTHING\n RETURNING username",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a80a18774baf36d09b07da1e4e30baab26b8160929aea6d8f5f226e6ec4f8bd8"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT consumed_at IS NOT NULL AS \"used!\" FROM login_link WHERE token_hash = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "used!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bpchar"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "ab16363a5225b022c7262f3caf5cd21ed1fbcc989fcc020c8246d5e2a313b72c"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO cloud_trial_offer (email, created_by) VALUES ($1, $2)\n ON CONFLICT (email) DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b2855a7bf20ec5a405d8c059e7b3b635507f545a514ff0d9142c663b779dd961"
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE v2_job_queue SET suspend = GREATEST(suspend - 1, 0) WHERE id = $1 RETURNING suspend",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "suspend",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b663e6baf2f8da00c6d94e5b8e35be9d9f51071978c0e48df4284d8b90000a4a"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS (SELECT 1 FROM usr WHERE workspace_id = $1 AND username = $2) AS \"claimed!\"",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "claimed!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "b675c20bb7a15bec5e9a34d7ddf347e22196e46727487f35634af079c71c2bef"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET ai_sessions_backup_generation = ai_sessions_backup_generation + 1 WHERE workspace_id = $1 AND large_file_storage IS NULL",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c2f3492c2d80f5c6d157d8c1dab7f9ed1c0f4b4d1f07789b40945cfb8a3a7b39"
|
||||
}
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE v2_job_status SET\n workflow_as_code_status = jsonb_set(\n jsonb_set(\n workflow_as_code_status,\n array[$1],\n COALESCE(workflow_as_code_status->$1, '{}'::jsonb)\n ),\n array[$1, 'duration_ms'],\n to_jsonb($2::bigint)\n )\n WHERE id = $3 AND workflow_as_code_status IS NOT NULL\n RETURNING workflow_as_code_status->'_checkpoint'->'pending_steps'->'job_ids' AS \"job_ids: serde_json::Value\"",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "job_ids: serde_json::Value",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "c331609952e0b98d36f605bd5d2933aa54523bf44d63e304440e53b9eadd5340"
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, name, kind, version, created_by, shared_at FROM ai_shared_artifact\n WHERE workspace_id = $1 AND email = $2 AND artifact_id = $3\n AND shared_at > now() - ($4::bigint::text || ' s')::interval",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "kind",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "version",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "created_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "shared_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "d2861932a739887785658cdf89a804306fe2083928e57458ba936d6afde53b57"
|
||||
}
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO resource_type\n (workspace_id, name, schema, description, created_by, format_extension, is_fileset, edited_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7, now())",
|
||||
"query": "INSERT INTO resource_type\n (workspace_id, name, schema, description, created_by, format_extension, is_fileset, display_name, edited_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, now())",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -11,10 +11,11 @@
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool"
|
||||
"Bool",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5899c7614f195fdd23e38389e52b004f957aafa2201b80638b5f87a625373f00"
|
||||
"hash": "dd6f4b505f4c1e2c734c5d04528c95bd6b3fb6ebf3ba115160573487cee4a606"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT large_file_storage IS NOT NULL AS \"has_storage!\" FROM workspace_settings WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "has_storage!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "dfa82a3f291cdc8f05cc4af6114e67c90e86d35dd5f74b1366f802dbb951ef9a"
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM ai_shared_artifact\n WHERE workspace_id = $1 AND id = $2 AND (email = $3 OR $4::bool)\n RETURNING name",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e50660f58274e9c135ace356ea8107739baa9ee276a99f481335fec8099f4d51"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT ai_sessions_backup_generation FROM workspace_settings WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "ai_sessions_backup_generation",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ea7bc2e5f53144ca23f8b3dea71cb6a994e969d915d50afbf0cb8547e6decf11"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO workspace_runnable_dependencies (flow_path, runnable_path, runnable_is_flow, runnable_is_agent, workspace_id) VALUES ($1, $2, FALSE, TRUE, $3) ON CONFLICT DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "fa8c36eda6d4cb64b4ac5979cc4eea76ab0226a514d25712a733e01add508b71"
|
||||
}
|
||||
Generated
+126
-122
@@ -549,7 +549,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"synstructure",
|
||||
"synstructure 0.13.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -561,7 +561,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"synstructure",
|
||||
"synstructure 0.13.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2425,9 +2425,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.6"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
|
||||
checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -2435,9 +2435,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.6"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
|
||||
checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -2447,9 +2447,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.4"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
|
||||
checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -2459,9 +2459,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
@@ -5177,9 +5177,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "frostem"
|
||||
version = "1.20260821.5"
|
||||
version = "1.20260821.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36a80a7406da302e04bfd2ca987907590d3a1f3c69958947c43890abd7426b2f"
|
||||
checksum = "d7d51501290db793146d5005edc29d3feac221da5702a4c63436344841ea88d4"
|
||||
|
||||
[[package]]
|
||||
name = "fs3"
|
||||
@@ -7442,9 +7442,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f"
|
||||
|
||||
[[package]]
|
||||
name = "lscolors"
|
||||
@@ -9851,9 +9851,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.11"
|
||||
version = "0.11.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
|
||||
checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
@@ -9871,9 +9871,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.17"
|
||||
version = "0.11.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83"
|
||||
checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"bytes",
|
||||
@@ -10572,9 +10572,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rmcp-macros"
|
||||
version = "3.3.0"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "873b730df6f0a9b74b13eb514e0dca4c2db0d8b68b74af98a2e9bf3f9d436585"
|
||||
checksum = "cd740c45d66ceb87e5579082abc27bd771665e464e9660a17a048c721b2a6025"
|
||||
dependencies = [
|
||||
"darling 0.24.1",
|
||||
"proc-macro2",
|
||||
@@ -12737,6 +12737,17 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.3.2"
|
||||
@@ -13059,9 +13070,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.3"
|
||||
version = "0.16.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b81c0cb5fce14f53e49c1d4da0c508334ff12040221bb8ab01b2dabd91d04b6e"
|
||||
checksum = "6ecfad6c3abc80a577f2b91c1e412ee57e7a060d430b553c1b0c940974ebcd49"
|
||||
dependencies = [
|
||||
"icu_segmenter",
|
||||
"unicode-width 0.2.2",
|
||||
@@ -13251,18 +13262,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.13.2"
|
||||
version = "1.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
|
||||
|
||||
[[package]]
|
||||
name = "tinyvector"
|
||||
@@ -14792,7 +14794,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -14880,7 +14882,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-ai"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
@@ -14913,7 +14915,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-alerting"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14926,7 +14928,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -15066,7 +15068,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-agent-workers"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15089,7 +15091,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-assets"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15106,7 +15108,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-auth"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -15132,7 +15134,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
@@ -15142,7 +15144,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-configs"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15159,7 +15161,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-debug"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"base64 0.22.1",
|
||||
@@ -15181,7 +15183,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-embeddings"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -15204,7 +15206,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flow-conversations"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15220,7 +15222,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flows"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15242,7 +15244,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-groups"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15253,6 +15255,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sql-builder",
|
||||
"sqlx",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"windmill-api-auth",
|
||||
"windmill-api-workspaces",
|
||||
@@ -15263,7 +15266,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-inputs"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15277,7 +15280,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-integration-tests"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -15312,7 +15315,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-jobs"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -15337,7 +15340,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-npm-proxy"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15365,7 +15368,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-openapi"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -15387,7 +15390,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-schedule"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15407,7 +15410,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-scripts"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15445,7 +15448,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-settings"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -15473,7 +15476,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-sse"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"serde",
|
||||
@@ -15485,7 +15488,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-users"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"axum 0.8.9",
|
||||
@@ -15509,7 +15512,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workers"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15523,7 +15526,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workspaces"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -15558,7 +15561,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
@@ -15572,7 +15575,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -15591,7 +15594,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"aho-corasick",
|
||||
@@ -15697,7 +15700,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-dep-map"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"futures",
|
||||
@@ -15717,7 +15720,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -15726,6 +15729,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"windmill-audit",
|
||||
"windmill-common",
|
||||
"windmill-dep-map",
|
||||
"windmill-queue",
|
||||
@@ -15733,7 +15737,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -15760,7 +15764,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-jseval"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -15777,7 +15781,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -15793,7 +15797,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-mcp"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15814,7 +15818,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-native-triggers"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15845,7 +15849,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-oauth"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -15870,7 +15874,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-object-store"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -15905,7 +15909,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-operator"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -15923,7 +15927,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -15932,7 +15936,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15944,7 +15948,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15956,7 +15960,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -15968,7 +15972,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15980,7 +15984,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15992,7 +15996,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -16003,7 +16007,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -16014,7 +16018,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -16026,7 +16030,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-asset"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -16037,7 +16041,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -16059,7 +16063,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-r"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -16071,7 +16075,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16085,7 +16089,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -16102,7 +16106,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16115,7 +16119,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql-asset"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -16127,7 +16131,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16145,7 +16149,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts-asset"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde-wasm-bindgen",
|
||||
@@ -16161,7 +16165,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wac"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -16177,7 +16181,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16191,7 +16195,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -16230,7 +16234,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-runtime-nativets"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"const_format",
|
||||
@@ -16270,7 +16274,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"wasm-bindgen",
|
||||
@@ -16281,7 +16285,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-store"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -16316,7 +16320,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-test-utils"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16340,7 +16344,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16373,7 +16377,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-amqp"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16400,7 +16404,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-azure"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16433,7 +16437,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-email"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16453,7 +16457,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-gcp"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16487,7 +16491,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-http"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16523,7 +16527,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-kafka"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16546,7 +16550,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-mqtt"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16570,7 +16574,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-nats"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -16594,7 +16598,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-postgres"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16629,7 +16633,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-sqs"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16657,7 +16661,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-websocket"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16682,7 +16686,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-types"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.13.2",
|
||||
@@ -16701,7 +16705,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-once-cell",
|
||||
@@ -16819,7 +16823,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker-volumes"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
@@ -17507,14 +17511,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
||||
checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"synstructure",
|
||||
"syn 3.0.5",
|
||||
"synstructure 0.14.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -17548,14 +17552,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom-derive"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
||||
checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"synstructure",
|
||||
"syn 3.0.5",
|
||||
"synstructure 0.14.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -17627,9 +17631,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.6.7"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
|
||||
checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.811.1"
|
||||
version = "1.813.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.811.1"
|
||||
version = "1.813.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
a4da009a5eae72bd55f34de41ba7929b53d53c9b
|
||||
d252afcc80e77fcc4f9a2a346b80908c8605a6c0
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS login_link;
|
||||
@@ -0,0 +1,13 @@
|
||||
-- Single-use login links minted by a superadmin for one account. Consumed by an
|
||||
-- unauthenticated GET that mints a session; the row is never a bearer credential itself.
|
||||
CREATE TABLE login_link (
|
||||
token_hash CHAR(64) PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL REFERENCES password(email) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
rd TEXT,
|
||||
expiration TIMESTAMPTZ NOT NULL,
|
||||
consumed_at TIMESTAMPTZ,
|
||||
created_by VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX login_link_email_idx ON login_link (email);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS cloud_trial_offer;
|
||||
@@ -0,0 +1,11 @@
|
||||
-- A pre-approved self-hosted Enterprise trial offered to an account created through a
|
||||
-- pre-approved invite. No expiry: the offer lasts until a trial or subscription exists.
|
||||
-- The cascade follows the account out on deletion and rename. A superadmin users-import
|
||||
-- replaces every account by deleting and reinserting it, which takes these rows with it:
|
||||
-- the offers, like the onboarding profiles, are recorded by the portal that minted them.
|
||||
CREATE TABLE cloud_trial_offer (
|
||||
email VARCHAR(255) PRIMARY KEY REFERENCES password(email) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
consumed_at TIMESTAMPTZ,
|
||||
created_by VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS cloud_onboarding_profile;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Context an invite carried about the account's owner, written at provisioning and read by
|
||||
-- onboarding to tailor itself (skip the source question it knows the answer to, later
|
||||
-- template picks and starter prompts). Free-form JSON so new fields need no migration.
|
||||
CREATE TABLE cloud_onboarding_profile (
|
||||
email VARCHAR(255) PRIMARY KEY REFERENCES password(email) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
profile JSONB NOT NULL,
|
||||
created_by VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE resource_type DROP COLUMN display_name;
|
||||
@@ -0,0 +1,24 @@
|
||||
-- The name a product goes by, beside the identifier a resource references: `gsheets` is
|
||||
-- "Google Sheets". Null where nobody named the type; readers derive a label from the name.
|
||||
ALTER TABLE resource_type ADD COLUMN display_name VARCHAR(100);
|
||||
|
||||
-- The names the hub carries today, so existing instances show them before any sync. Only in
|
||||
-- admins, where hub resource types live and every workspace reads them from.
|
||||
UPDATE resource_type SET display_name = v.display_name
|
||||
FROM (VALUES
|
||||
('bamboo_hr', 'BambooHR'),
|
||||
('cacertificate', 'CA certificate'),
|
||||
('deep_infra', 'DeepInfra'),
|
||||
('gcal', 'Google Calendar'),
|
||||
('gdocs', 'Google Docs'),
|
||||
('gdrive', 'Google Drive'),
|
||||
('gforms', 'Google Forms'),
|
||||
('gsheets', 'Google Sheets'),
|
||||
('gworkspace', 'Google Workspace'),
|
||||
('sensortower', 'Sensor Tower'),
|
||||
('snowflake_oauth', 'Snowflake (OAuth)'),
|
||||
('their_stack', 'TheirStack')
|
||||
) AS v(name, display_name)
|
||||
WHERE resource_type.workspace_id = 'admins'
|
||||
AND resource_type.name = v.name
|
||||
AND resource_type.display_name IS NULL;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS ai_shared_artifact;
|
||||
@@ -0,0 +1,32 @@
|
||||
-- A copy of an AI session artifact that its author explicitly shared with the workspace.
|
||||
-- Artifacts otherwise live only in the author's browser; this row exists only while the
|
||||
-- share does, and the monitor deletes it once `shared_at` falls outside
|
||||
-- AI_SHARED_ARTIFACT_RETENTION_SECS.
|
||||
CREATE TABLE ai_shared_artifact (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id) ON DELETE CASCADE,
|
||||
-- The browser-side artifact id. Unique per author so sharing the same artifact again
|
||||
-- moves its one link forward rather than minting a second one.
|
||||
artifact_id VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
created_by VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
kind VARCHAR(10) NOT NULL CHECK (kind IN ('md', 'html')),
|
||||
version INTEGER NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
-- Reset on every re-share: retention counts from the last time the author shared it.
|
||||
shared_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (workspace_id, email, artifact_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_ai_shared_artifact_shared_at ON ai_shared_artifact (shared_at);
|
||||
|
||||
GRANT ALL ON ai_shared_artifact TO windmill_admin;
|
||||
GRANT ALL ON ai_shared_artifact TO windmill_user;
|
||||
|
||||
-- The handlers go through the raw pool and scope every query to the workspace themselves.
|
||||
-- An admin-only policy is the backstop for a future query that reaches this table through
|
||||
-- UserDB.
|
||||
ALTER TABLE ai_shared_artifact ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY admin_policy ON ai_shared_artifact FOR ALL TO windmill_admin USING (true);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE workspace_settings DROP COLUMN IF EXISTS ai_sessions_backup_generation;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Bumped by every workspace key rotation: the AI session backups in the workspace storage
|
||||
-- live under a prefix named by it, so a rotation moves to a fresh prefix and the previous
|
||||
-- ones can be deleted at leisure without ever touching live objects.
|
||||
ALTER TABLE workspace_settings ADD COLUMN ai_sessions_backup_generation BIGINT NOT NULL DEFAULT 0;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
DELETE FROM workspace_runnable_dependencies WHERE runnable_is_agent;
|
||||
|
||||
DROP INDEX flow_workspace_without_hash_unique_idx;
|
||||
|
||||
CREATE UNIQUE INDEX flow_workspace_without_hash_unique_idx
|
||||
ON workspace_runnable_dependencies (flow_path, runnable_path, runnable_is_flow, workspace_id)
|
||||
WHERE script_hash IS NULL;
|
||||
|
||||
ALTER TABLE workspace_runnable_dependencies DROP COLUMN runnable_is_agent;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
-- A flow step linked to a saved agent (an `ai_agent` resource) is recorded next to the scripts and
|
||||
-- subflows the flow runs, so renaming the agent can name the flows it would break. An agent row is
|
||||
-- neither a script nor a flow: readers of script usages have to exclude it.
|
||||
ALTER TABLE workspace_runnable_dependencies
|
||||
ADD COLUMN runnable_is_agent BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- A script step and a linked agent can share a path. Without the flag in the key, the second
|
||||
-- insert's ON CONFLICT DO NOTHING would silently drop one of the two rows.
|
||||
DROP INDEX flow_workspace_without_hash_unique_idx;
|
||||
|
||||
CREATE UNIQUE INDEX flow_workspace_without_hash_unique_idx
|
||||
ON workspace_runnable_dependencies (flow_path, runnable_path, runnable_is_flow, runnable_is_agent, workspace_id)
|
||||
WHERE script_hash IS NULL;
|
||||
|
||||
-- The worker only records a flow when it is next deployed, so seed the ones already linking an
|
||||
-- agent from their current value.
|
||||
INSERT INTO workspace_runnable_dependencies (flow_path, runnable_path, runnable_is_flow, runnable_is_agent, workspace_id)
|
||||
SELECT DISTINCT f.path, agent_ref #>> '{}', false, true, f.workspace_id
|
||||
FROM flow f
|
||||
CROSS JOIN LATERAL jsonb_path_query(f.value, 'lax $.** ? (@.type == "aiagent" && @.agent.type() == "string").agent') AS agent_ref
|
||||
ON CONFLICT DO NOTHING;
|
||||
+24
-24
@@ -6191,7 +6191,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -6274,7 +6274,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -6286,7 +6286,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"serde",
|
||||
@@ -6295,7 +6295,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6307,7 +6307,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6319,7 +6319,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -6331,7 +6331,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6343,7 +6343,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6355,7 +6355,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -6366,7 +6366,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -6377,7 +6377,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -6389,7 +6389,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-asset"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -6400,7 +6400,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -6422,7 +6422,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-r"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6434,7 +6434,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6448,7 +6448,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case",
|
||||
@@ -6465,7 +6465,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6478,7 +6478,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql-asset"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -6490,7 +6490,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6508,7 +6508,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts-asset"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde-wasm-bindgen",
|
||||
@@ -6524,7 +6524,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wac"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -6540,7 +6540,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wasm"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"getrandom 0.2.17",
|
||||
@@ -6572,7 +6572,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6586,7 +6586,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-types"
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
|
||||
@@ -12,7 +12,7 @@ resolver = "2"
|
||||
members = ["."]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.811.1"
|
||||
version = "1.813.0"
|
||||
edition = "2021"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
|
||||
|
||||
+56
-11
@@ -411,6 +411,13 @@ struct HubResourceTypeRaw {
|
||||
/// Absent from hubs predating the column, and from caches written before it.
|
||||
#[serde(default)]
|
||||
pub format_extension: Option<String>,
|
||||
/// Doubly optional, so a hub predating the field (no key) is told apart from a type the
|
||||
/// hub leaves unnamed (null).
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "windmill_common::more_serde::double_option"
|
||||
)]
|
||||
pub display_name: Option<Option<String>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -434,6 +441,14 @@ pub struct HubResourceType {
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub format_extension: Option<Option<String>>,
|
||||
/// Doubly optional like `format_extension`: a cache written before the field leaves the
|
||||
/// stored name alone, while a null from the hub clears it.
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "windmill_common::more_serde::double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub display_name: Option<Option<String>>,
|
||||
}
|
||||
|
||||
const HUB_RT_CACHE_FILE: &str = "resource_types.json";
|
||||
@@ -481,6 +496,7 @@ async fn cache_hub_resource_types() -> anyhow::Result<()> {
|
||||
app: rt.app,
|
||||
description: rt.description,
|
||||
format_extension: Some(rt.format_extension),
|
||||
display_name: rt.display_name,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -531,8 +547,9 @@ pub async fn sync_cached_resource_types(db: &sqlx::Pool<sqlx::Postgres>) -> anyh
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
bool,
|
||||
Option<String>,
|
||||
)> = sqlx::query_as(
|
||||
"SELECT name, schema, description, format_extension, is_fileset FROM resource_type WHERE workspace_id = 'admins'",
|
||||
"SELECT name, schema, description, format_extension, is_fileset, display_name FROM resource_type WHERE workspace_id = 'admins'",
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
@@ -540,12 +557,23 @@ pub async fn sync_cached_resource_types(db: &sqlx::Pool<sqlx::Postgres>) -> anyh
|
||||
|
||||
let existing_map: std::collections::HashMap<
|
||||
String,
|
||||
(Option<serde_json::Value>, Option<String>, Option<String>, bool),
|
||||
(
|
||||
Option<serde_json::Value>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
bool,
|
||||
Option<String>,
|
||||
),
|
||||
> = existing_types
|
||||
.into_iter()
|
||||
.map(|(name, schema, desc, format_extension, is_fileset)| {
|
||||
(name, (schema, desc, format_extension, is_fileset))
|
||||
})
|
||||
.map(
|
||||
|(name, schema, desc, format_extension, is_fileset, display_name)| {
|
||||
(
|
||||
name,
|
||||
(schema, desc, format_extension, is_fileset, display_name),
|
||||
)
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
|
||||
let mut synced_count = 0;
|
||||
@@ -553,8 +581,9 @@ pub async fn sync_cached_resource_types(db: &sqlx::Pool<sqlx::Postgres>) -> anyh
|
||||
|
||||
for rt in cached_types {
|
||||
let existing = existing_map.get(&rt.name);
|
||||
let is_fileset = existing.map(|(_, _, _, f)| *f).unwrap_or(false);
|
||||
let stored_extension = existing.and_then(|(_, _, e, _)| e.clone());
|
||||
let is_fileset = existing.map(|(_, _, _, f, _)| *f).unwrap_or(false);
|
||||
let stored_extension = existing.and_then(|(_, _, e, _, _)| e.clone());
|
||||
let stored_display_name = existing.and_then(|(_, _, _, _, n)| n.clone());
|
||||
// A fileset is a set of files, so it cannot also be one file. Create, update
|
||||
// and the manual sync all reject the pair; this writer would otherwise
|
||||
// persist it onto a same-named local fileset.
|
||||
@@ -570,11 +599,25 @@ pub async fn sync_cached_resource_types(db: &sqlx::Pool<sqlx::Postgres>) -> anyh
|
||||
None => stored_extension.clone(),
|
||||
}
|
||||
};
|
||||
// No key in the cache leaves the stored name alone, as for the extension. So does a name
|
||||
// too long for the column: one bad entry must not fail the upsert and end the sync.
|
||||
let display_name = match &rt.display_name {
|
||||
Some(Some(name)) if name.chars().count() > 100 => {
|
||||
tracing::warn!(
|
||||
"Ignoring the display_name of resource type {}: longer than 100 characters",
|
||||
rt.name
|
||||
);
|
||||
stored_display_name.clone()
|
||||
}
|
||||
Some(from_cache) => from_cache.clone(),
|
||||
None => stored_display_name.clone(),
|
||||
};
|
||||
|
||||
if let Some((existing_schema, existing_desc, _, _)) = existing {
|
||||
if let Some((existing_schema, existing_desc, _, _, _)) = existing {
|
||||
if existing_schema == &rt.schema
|
||||
&& existing_desc == &rt.description
|
||||
&& stored_extension == format_extension
|
||||
&& stored_display_name == display_name
|
||||
{
|
||||
skipped_count += 1;
|
||||
continue;
|
||||
@@ -586,16 +629,18 @@ pub async fn sync_cached_resource_types(db: &sqlx::Pool<sqlx::Postgres>) -> anyh
|
||||
// `format_extension` is resolved above rather than coalesced here: a
|
||||
// COALESCE could never clear one, so a hub that dropped an extension
|
||||
// would leave the stale value behind forever.
|
||||
"INSERT INTO resource_type (workspace_id, name, schema, description, format_extension, edited_at)
|
||||
VALUES ('admins', $1, $2, $3, $4, now())
|
||||
"INSERT INTO resource_type (workspace_id, name, schema, description, format_extension, display_name, edited_at)
|
||||
VALUES ('admins', $1, $2, $3, $4, $5, now())
|
||||
ON CONFLICT (workspace_id, name) DO UPDATE
|
||||
SET schema = EXCLUDED.schema, description = EXCLUDED.description,
|
||||
format_extension = EXCLUDED.format_extension, edited_at = now()",
|
||||
format_extension = EXCLUDED.format_extension,
|
||||
display_name = EXCLUDED.display_name, edited_at = now()",
|
||||
)
|
||||
.bind(&rt.name)
|
||||
.bind(&rt.schema)
|
||||
.bind(&rt.description)
|
||||
.bind(&format_extension)
|
||||
.bind(&display_name)
|
||||
.execute(db)
|
||||
.await
|
||||
.with_context(|| format!("Failed to upsert resource type {}", rt.name))?;
|
||||
|
||||
+90
-6
@@ -1784,6 +1784,23 @@ pub async fn delete_expired_items(db: &DB) -> () {
|
||||
Err(e) => tracing::error!("Error deleting token: {}", e.to_string()),
|
||||
}
|
||||
|
||||
let expired_login_links_r: std::result::Result<Vec<String>, _> =
|
||||
// Expired rows stay a day so an open still reports "expired" rather than "invalid".
|
||||
sqlx::query_scalar(
|
||||
"DELETE FROM login_link WHERE expiration <= now() - interval '1 day' RETURNING token_hash",
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await;
|
||||
|
||||
match expired_login_links_r {
|
||||
Ok(hashes) => {
|
||||
if !hashes.is_empty() {
|
||||
tracing::info!("deleted {} expired login links", hashes.len())
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::error!("Error deleting login links: {}", e.to_string()),
|
||||
}
|
||||
|
||||
let pip_resolution_r = sqlx::query_scalar!(
|
||||
"DELETE FROM pip_resolution_cache WHERE expiration <= now() RETURNING hash",
|
||||
)
|
||||
@@ -1895,6 +1912,17 @@ pub async fn delete_expired_items(db: &DB) -> () {
|
||||
tracing::info!("deleted {} expired otel trace spans", deleted_spans);
|
||||
}
|
||||
|
||||
if let Err(e) = sqlx::query!(
|
||||
"DELETE FROM ai_shared_artifact
|
||||
WHERE shared_at <= now() - ($1::bigint::text || ' s')::interval",
|
||||
windmill_common::ai_shared_artifact_retention_secs(),
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error deleting expired shared AI artifacts: {:?}", e);
|
||||
}
|
||||
|
||||
let audit_retention_days = audit_log_retention_days().await;
|
||||
let audit_retention_secs: i64 = audit_retention_days * 60 * 60 * 24;
|
||||
|
||||
@@ -4352,6 +4380,23 @@ pub async fn monitor_db(
|
||||
}
|
||||
};
|
||||
|
||||
// Delete the AI session backups older than their workspace's retention. Every ~40 min
|
||||
// (240 iterations at the default 10 s, the most a u8 `should_run` counts): the retention
|
||||
// counts in days. Spawned for the same reason as the credential maintenance above, a
|
||||
// sweep of many sessions outlasting the join's deadline; the sweep's own advisory lock
|
||||
// keeps one server at a time at it.
|
||||
let ai_session_retention_f = async {
|
||||
#[cfg(feature = "parquet")]
|
||||
if server_mode && iteration.is_some() && iteration.as_ref().unwrap().should_run(240) {
|
||||
if let Some(db) = conn.as_sql() {
|
||||
let db = db.clone();
|
||||
tokio::spawn(
|
||||
async move { windmill_api::sweep_expired_ai_session_backups(&db).await },
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// run every 2 iterations (~20s at the default LISTEN_NEW_EVENTS_INTERVAL_SEC).
|
||||
// Enterprise feature: the active `// freshness` backstop lives in
|
||||
// windmill-queue's `freshness_watchdog` (`private`); OSS gets a no-op stub.
|
||||
@@ -4406,6 +4451,7 @@ pub async fn monitor_db(
|
||||
cleanup_scheduled_job_deletions_f,
|
||||
git_auto_pull_f,
|
||||
git_credential_maintenance_f,
|
||||
ai_session_retention_f,
|
||||
pipeline_freshness_watchdog_f,
|
||||
reconcile_unarmed_schedules_f,
|
||||
);
|
||||
@@ -6151,7 +6197,10 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, node_n
|
||||
/// Force-complete a zombie job that handle_job_error failed to complete.
|
||||
/// This is a minimal fallback: it inserts a failed completed job and deletes
|
||||
/// from the queue in a single transaction, without schedule pushing or
|
||||
/// error handler logic that could cause the completion to fail.
|
||||
/// error handler logic. The one thing it keeps is the WAC parent notification,
|
||||
/// deliberately inside the transaction: if that fails, the whole completion
|
||||
/// rolls back and the job waits for the next sweep, which is cheaper than a
|
||||
/// parent parked for its full suspend window and a task run twice.
|
||||
async fn force_complete_zombie_job(
|
||||
db: &Pool<Postgres>,
|
||||
job_id: &Uuid,
|
||||
@@ -6173,14 +6222,18 @@ async fn force_complete_zombie_job(
|
||||
"Zombie job {job_id} was not completed by handle_job_error, force-completing it"
|
||||
);
|
||||
|
||||
// Same `{"error": ...}` shape as every other failed job's result, so a WAC
|
||||
// parent's failure record reads the name and message like any task failure.
|
||||
let error_value = serde_json::json!({
|
||||
"message": error_message,
|
||||
"name": "ExecutionErr",
|
||||
"error": {
|
||||
"message": error_message,
|
||||
"name": "ExecutionErr",
|
||||
}
|
||||
});
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
let duration_ms = sqlx::query_scalar!(
|
||||
"INSERT INTO v2_job_completed
|
||||
(workspace_id, id, started_at, duration_ms, result, memory_peak, status, worker)
|
||||
SELECT q.workspace_id, q.id, q.started_at,
|
||||
@@ -6189,19 +6242,50 @@ async fn force_complete_zombie_job(
|
||||
FROM v2_job_queue q
|
||||
LEFT JOIN v2_job_runtime r ON r.id = q.id
|
||||
WHERE q.id = $1
|
||||
ON CONFLICT (id) DO UPDATE SET status = 'failure', result = $2::jsonb",
|
||||
ON CONFLICT (id) DO UPDATE SET status = 'failure', result = $2::jsonb
|
||||
RETURNING duration_ms AS \"duration_ms!\"",
|
||||
job_id,
|
||||
error_value,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
// A WAC parent parked on this job must learn of the failure here too, or it
|
||||
// waits out its whole suspend window and runs the task again.
|
||||
let mut wac_parent_ready = false;
|
||||
if let Some(duration_ms) = duration_ms {
|
||||
let parent = sqlx::query!(
|
||||
"SELECT parent_job, flow_step_id FROM v2_job WHERE id = $1",
|
||||
job_id
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
if let Some(parent_job) = parent
|
||||
.filter(|j| j.flow_step_id.is_none())
|
||||
.and_then(|j| j.parent_job)
|
||||
{
|
||||
wac_parent_ready = windmill_common::wac::record_child_completion(
|
||||
&mut tx,
|
||||
&parent_job,
|
||||
job_id,
|
||||
false,
|
||||
duration_ms,
|
||||
&error_value.to_string(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
sqlx::query!("DELETE FROM v2_job_queue WHERE id = $1", job_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
if wac_parent_ready {
|
||||
windmill_common::wac::WAC_SUSPEND_READY.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
tracing::info!("Force-completed zombie job {job_id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ agent_token_blacklist: token(char), expires_at(ts), blacklisted_at(ts), blacklis
|
||||
ai_agent_memory: workspace_id(char), conversation_id(uuid), step_id(char), messages(jsonb), created_at(ts), updated_at(ts)
|
||||
ai_free_token_daily_usage: day(date), cost_nanos(bigint), updated_at(ts)
|
||||
ai_free_token_usage: email(char), cost_nanos(bigint), updated_at(ts)
|
||||
ai_shared_artifact: id(uuid), workspace_id(char), artifact_id(char), email(char), created_by(char), name(char), kind(char), version(int), content(text), shared_at(ts)
|
||||
FK: (workspace_id) -> workspace(id)
|
||||
ai_token_usage: workspace_id(char), day(date), email(char), provider(char), model(char), session_id(char), input_tokens(bigint), cache_read_tokens(bigint), cache_write_tokens(bigint), output_tokens(bigint), reported_cost_nano_usd(bigint), requests(bigint), updated_at(ts)
|
||||
FK: (workspace_id) -> workspace(id)
|
||||
alerts: id(int), alert_type(char), message(text), created_at(ts), acknowledged(bool), workspace_id(text), acknowledged_workspace(bool), resource(text)
|
||||
@@ -171,7 +173,7 @@ raw_app: path(char), version(int), workspace_id(char), summary(char), edited_at(
|
||||
FK: (workspace_id) -> workspace(id)
|
||||
resource: workspace_id(char), path(char), value(jsonb), description(text), resource_type(char), extra_perms(jsonb), edited_at(ts), created_by(char), labels(text[])
|
||||
FK: (workspace_id) -> workspace(id)
|
||||
resource_type: workspace_id(char), name(char), schema(jsonb), description(text), edited_at(ts), created_by(char), format_extension(char), is_fileset(bool)
|
||||
resource_type: workspace_id(char), name(char), schema(jsonb), description(text), edited_at(ts), created_by(char), format_extension(char), is_fileset(bool), display_name(char)
|
||||
FK: (workspace_id) -> workspace(id)
|
||||
resume_job: id(uuid), job(uuid), flow(uuid), created_at(ts), value(jsonb), approver(char), resume_id(int), approved(bool)
|
||||
FK: (flow) -> v2_job_queue(id)
|
||||
@@ -230,9 +232,9 @@ workspace_key: workspace_id(char), kind(workspace_key_kind), key(char)
|
||||
FK: (workspace_id) -> workspace(id)
|
||||
workspace_protection_rule: workspace_id(char), name(char), rules(int), bypass_groups(text[]), bypass_users(text[]), created_at(ts)
|
||||
FK: (workspace_id) -> workspace(id)
|
||||
workspace_runnable_dependencies: flow_path(char), runnable_path(char), script_hash(bigint), runnable_is_flow(bool), workspace_id(char), app_path(char), id(bigint)
|
||||
workspace_runnable_dependencies: flow_path(char), runnable_path(char), script_hash(bigint), runnable_is_flow(bool), workspace_id(char), app_path(char), id(bigint), runnable_is_agent(bool)
|
||||
FK: (app_path, workspace_id) -> app(path, workspace_id) | (flow_path, workspace_id) -> flow(path, workspace_id)
|
||||
workspace_settings: workspace_id(char), slack_team_id(char), slack_name(char), slack_command_script(char), slack_email(char), customer_id(char), plan(char), webhook(text), ai_config(jsonb), large_file_storage(jsonb), git_sync(jsonb), default_app(char), default_scripts(jsonb), deploy_ui(jsonb), mute_critical_alerts(bool), color(char), operator_settings(jsonb), teams_command_script(text), teams_team_id(text), teams_team_name(text), git_app_installations(jsonb), ducklake(jsonb), slack_oauth_client_id(char), slack_oauth_client_secret(char), datatable(jsonb), teams_team_guid(text), auto_invite(jsonb), error_handler(jsonb), success_handler(jsonb), public_app_execution_limit_per_minute(int), dbt_warehouses(jsonb), guest_access_enabled(bool), guest_jwt_public_key(text), guest_jwt_jwks_url(text)
|
||||
workspace_settings: workspace_id(char), slack_team_id(char), slack_name(char), slack_command_script(char), slack_email(char), customer_id(char), plan(char), webhook(text), ai_config(jsonb), large_file_storage(jsonb), git_sync(jsonb), default_app(char), default_scripts(jsonb), deploy_ui(jsonb), mute_critical_alerts(bool), color(char), operator_settings(jsonb), teams_command_script(text), teams_team_id(text), teams_team_name(text), git_app_installations(jsonb), ducklake(jsonb), slack_oauth_client_id(char), slack_oauth_client_secret(char), datatable(jsonb), teams_team_guid(text), auto_invite(jsonb), error_handler(jsonb), success_handler(jsonb), public_app_execution_limit_per_minute(int), dbt_warehouses(jsonb), guest_access_enabled(bool), guest_jwt_public_key(text), guest_jwt_jwks_url(text), ai_sessions_backup_generation(int)
|
||||
FK: (workspace_id) -> workspace(id)
|
||||
zombie_job_counter: job_id(uuid), counter(int)
|
||||
FK: (job_id) -> v2_job(id)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,184 @@
|
||||
//! Shared AI session artifacts: one link per author and artifact, readable by any workspace
|
||||
//! member until its retention window passes, and removable only by its author or an admin.
|
||||
//!
|
||||
//! Expiry is enforced on read as well as by the monitor's sweep, so a share past its window must
|
||||
//! not be served in the gap before the sweep reaches it.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_test_utils::*;
|
||||
|
||||
const ADMIN: &str = "Bearer SECRET_TOKEN";
|
||||
const MEMBER: &str = "Bearer SECRET_TOKEN_2";
|
||||
|
||||
async fn share(
|
||||
client: &reqwest::Client,
|
||||
base: &str,
|
||||
token: &str,
|
||||
content: &str,
|
||||
) -> anyhow::Result<Value> {
|
||||
let resp = client
|
||||
.post(format!("{base}/share"))
|
||||
.header("Authorization", token)
|
||||
.json(&json!({
|
||||
"artifact_id": "plan:session-1",
|
||||
"name": "Plan",
|
||||
"kind": "md",
|
||||
"version": 1,
|
||||
"content": content,
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 200, "{}", resp.text().await?);
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base", "ai_shared_artifacts"))]
|
||||
async fn shared_artifact_is_served_to_members_until_it_expires(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let base = format!(
|
||||
"http://localhost:{}/api/w/test-workspace/ai/shared_artifacts",
|
||||
server.addr.port()
|
||||
);
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let first = share(&client, &base, MEMBER, "draft").await?;
|
||||
let second = share(&client, &base, MEMBER, "final").await?;
|
||||
assert_eq!(first["id"], second["id"], "re-sharing minted a second link");
|
||||
let id = second["id"].as_str().unwrap();
|
||||
|
||||
let resp = client
|
||||
.get(format!("{base}/get/{id}"))
|
||||
.header("Authorization", ADMIN)
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 200);
|
||||
let body: Value = resp.json().await?;
|
||||
assert_eq!(body["content"], "final");
|
||||
|
||||
// The handlers read through the raw pool, so the workspace in the URL is the only thing
|
||||
// scoping a share: a member of another workspace must not reach it by id through theirs.
|
||||
let resp = client
|
||||
.get(format!(
|
||||
"http://localhost:{}/api/w/test-workspace-2/ai/shared_artifacts/get/{id}",
|
||||
server.addr.port()
|
||||
))
|
||||
.header("Authorization", ADMIN)
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(
|
||||
resp.status(),
|
||||
404,
|
||||
"a share was served through another workspace's path"
|
||||
);
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE ai_shared_artifact SET shared_at = now() - ($1::bigint + 60) * interval '1 second'",
|
||||
)
|
||||
.bind(windmill_common::ai_shared_artifact_retention_secs())
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let resp = client
|
||||
.get(format!("{base}/get/{id}"))
|
||||
.header("Authorization", ADMIN)
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 404, "an expired share was served");
|
||||
|
||||
let status: Value = client
|
||||
.get(format!("{base}/status?artifact_id=plan:session-1"))
|
||||
.header("Authorization", MEMBER)
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
assert!(
|
||||
status.get("share").is_none(),
|
||||
"an expired share was reported live: {status}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn only_the_author_or_an_admin_can_unshare(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let base = format!(
|
||||
"http://localhost:{}/api/w/test-workspace/ai/shared_artifacts",
|
||||
server.addr.port()
|
||||
);
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let shared = share(&client, &base, ADMIN, "admin's plan").await?;
|
||||
let id = shared["id"].as_str().unwrap();
|
||||
|
||||
let resp = client
|
||||
.delete(format!("{base}/delete/{id}"))
|
||||
.header("Authorization", MEMBER)
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 404);
|
||||
let remaining: i64 = sqlx::query_scalar("SELECT count(*) FROM ai_shared_artifact")
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
assert_eq!(remaining, 1, "a member deleted someone else's share");
|
||||
|
||||
let member_share = share(&client, &base, MEMBER, "member's plan").await?;
|
||||
let resp = client
|
||||
.delete(format!(
|
||||
"{base}/delete/{}",
|
||||
member_share["id"].as_str().unwrap()
|
||||
))
|
||||
.header("Authorization", ADMIN)
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 200, "{}", resp.text().await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The id is compared against a `VARCHAR(255)` column on every route that takes one, and a
|
||||
/// NUL in it would otherwise reach Postgres and come back as a 500.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn a_malformed_artifact_id_is_refused_on_every_route(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let base = format!(
|
||||
"http://localhost:{}/api/w/test-workspace/ai/shared_artifacts",
|
||||
server.addr.port()
|
||||
);
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
for bad_id in ["", "a\0b", &"x".repeat(256)] {
|
||||
let resp = client
|
||||
.get(format!("{base}/status"))
|
||||
.query(&[("artifact_id", bad_id)])
|
||||
.header("Authorization", MEMBER)
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 400, "status accepted {bad_id:?}");
|
||||
|
||||
let resp = client
|
||||
.post(format!("{base}/share"))
|
||||
.header("Authorization", MEMBER)
|
||||
.json(&json!({
|
||||
"artifact_id": bad_id,
|
||||
"name": "Plan",
|
||||
"kind": "md",
|
||||
"version": 1,
|
||||
"content": "x",
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 400, "share accepted {bad_id:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
//! Regression: run mode of `execute_component`'s no-id inline-`raw_code` arm runs
|
||||
//! *only* the `rawscript/<sha>`-pinned `content`, dropping the caller `hash`,
|
||||
//! `lock`, `modules` and `dedicated_worker` and deriving `path` server-side —
|
||||
//! all of which would otherwise run or install unpinned code as the app identity.
|
||||
//! Preview mode keeps honoring the caller's fields.
|
||||
|
||||
use serde_json::json;
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_test_utils::*;
|
||||
|
||||
fn client() -> reqwest::Client {
|
||||
reqwest::Client::new()
|
||||
}
|
||||
|
||||
fn authed(b: reqwest::RequestBuilder, token: &str) -> reqwest::RequestBuilder {
|
||||
b.header("Authorization", format!("Bearer {}", token))
|
||||
}
|
||||
|
||||
const CONTENT: &str = "print('benign')\n";
|
||||
// A caller lock whose presence is the whole point: if it reaches the job, the
|
||||
// worker installs it. The value only needs to be recognizable in `v2_job`.
|
||||
const CALLER_LOCK: &str = "evilpkg @ file:///tmp/attacker-controlled-sdist";
|
||||
// A non-codebase-sentinel hash: if it reaches the job as `runnable_id`, the
|
||||
// worker fetches (and runs) a deployed script by hash instead of the pinned
|
||||
// content. It need not resolve to a real row — the guard is that it never
|
||||
// becomes `runnable_id`.
|
||||
const CALLER_HASH: i64 = 123456789;
|
||||
// A caller path in someone else's namespace: if it reaches the job as
|
||||
// `runnable_path` it redirects where the pinned content's relative imports
|
||||
// resolve. Run mode must instead derive the path from `<app_path>/<component>`.
|
||||
const CALLER_PATH: &str = "u/attacker/evil/comp";
|
||||
|
||||
/// The pin key `execute_component` computes for a no-id inline script:
|
||||
/// `rawscript/<sha256(content)>`.
|
||||
fn rawscript_pin(content: &str) -> String {
|
||||
let mut h = Sha256::new();
|
||||
h.update(content);
|
||||
format!("rawscript/{:x}", h.finalize())
|
||||
}
|
||||
|
||||
fn inline_raw_code(hash: Option<i64>, dedicated: bool) -> serde_json::Value {
|
||||
let mut rc = json!({
|
||||
"language": "python3",
|
||||
"content": CONTENT,
|
||||
"path": CALLER_PATH,
|
||||
"lock": CALLER_LOCK,
|
||||
"modules": {
|
||||
"m.py": { "content": "print('x')\n", "language": "python3", "lock": CALLER_LOCK }
|
||||
}
|
||||
});
|
||||
if let Some(h) = hash {
|
||||
rc["hash"] = json!(h);
|
||||
}
|
||||
if dedicated {
|
||||
rc["dedicated_worker"] = json!(true);
|
||||
}
|
||||
rc
|
||||
}
|
||||
|
||||
/// Fetch `(raw_lock, args-has-_MODULES, runnable_id, tag, runnable_path)` for an
|
||||
/// enqueued job.
|
||||
async fn job_fields(
|
||||
db: &Pool<Postgres>,
|
||||
uuid: uuid::Uuid,
|
||||
) -> anyhow::Result<(Option<String>, bool, Option<i64>, String, Option<String>)> {
|
||||
Ok(sqlx::query_as(
|
||||
"SELECT raw_lock, (args ? '_MODULES'), runnable_id, tag, runnable_path \
|
||||
FROM v2_job WHERE id = $1",
|
||||
)
|
||||
.bind(uuid)
|
||||
.fetch_one(db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_run_mode_strips_caller_lock_and_modules(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
let ws = format!("http://localhost:{port}/api/w/test-workspace");
|
||||
|
||||
let app_path = "u/test-user/lockstrip";
|
||||
let pin = format!("comp:{}", rawscript_pin(CONTENT));
|
||||
|
||||
// Deployed Viewer-mode app whose only runnable is an inline script pinned by
|
||||
// content hash and with no `app_script` row — the legacy `rawscript/<sha>`
|
||||
// case that reaches the no-id run-mode arm this fix touches.
|
||||
let resp = authed(client().post(format!("{ws}/apps/create")), "SECRET_TOKEN")
|
||||
.json(&json!({
|
||||
"path": app_path,
|
||||
"summary": "",
|
||||
"value": {},
|
||||
"policy": {
|
||||
"execution_mode": "viewer",
|
||||
"triggerables_v2": { pin: { "static_inputs": {}, "one_of_inputs": {} } }
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 201, "create app: {}", resp.text().await?);
|
||||
|
||||
// Run mode (no `force_viewer_static_fields`): the pin authorizes the run, but
|
||||
// every caller field that selects, installs, or routes code — hash, lock,
|
||||
// modules, dedicated_worker — must be dropped.
|
||||
let resp = authed(
|
||||
client().post(format!("{ws}/apps_u/execute_component/{app_path}")),
|
||||
"SECRET_TOKEN_2",
|
||||
)
|
||||
.json(&json!({
|
||||
// The args map is the other injection channel: an inline run is a
|
||||
// `JobKind::Preview` job, so the worker/executors read `_MODULES` and
|
||||
// `_TEMP_SCRIPT_REFS` back out of the job args. Both must be stripped.
|
||||
"args": {
|
||||
"_MODULES": { "m.py": { "content": "print('evil')\n", "language": "python3" } },
|
||||
"_TEMP_SCRIPT_REFS": { "../evil": "deadbeef" }
|
||||
},
|
||||
"component": "comp",
|
||||
"raw_code": inline_raw_code(Some(CALLER_HASH), true)
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
let status = resp.status();
|
||||
let body = resp.text().await?;
|
||||
assert_eq!(
|
||||
status, 200,
|
||||
"run-mode pinned inline run must be accepted: {body}"
|
||||
);
|
||||
let uuid = uuid::Uuid::parse_str(body.trim())?;
|
||||
let (raw_lock, has_modules, runnable_id, tag, runnable_path) = job_fields(&db, uuid).await?;
|
||||
assert_eq!(
|
||||
raw_lock, None,
|
||||
"run mode must strip the caller-supplied lock"
|
||||
);
|
||||
assert!(
|
||||
!has_modules,
|
||||
"run mode must strip caller modules (both `raw_code.modules` and an `_MODULES` arg)"
|
||||
);
|
||||
let has_temp_refs: bool =
|
||||
sqlx::query_scalar("SELECT (args ? '_TEMP_SCRIPT_REFS') FROM v2_job WHERE id = $1")
|
||||
.bind(uuid)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
assert!(
|
||||
!has_temp_refs,
|
||||
"run mode must strip a caller `_TEMP_SCRIPT_REFS` arg (relative-import redirect)"
|
||||
);
|
||||
assert_eq!(
|
||||
runnable_id, None,
|
||||
"run mode must strip the caller-supplied hash (no substituting a deployed script by hash)"
|
||||
);
|
||||
assert!(
|
||||
!tag.starts_with("dedi:"),
|
||||
"run mode must strip caller `dedicated_worker` (no routing to a path-keyed dedicated worker), got tag {tag:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
runnable_path.as_deref(),
|
||||
Some(format!("{app_path}/comp").as_str()),
|
||||
"run mode must derive the path server-side, not trust the caller's (relative-import base)"
|
||||
);
|
||||
|
||||
// Preview mode (editor): the caller runs their own code as themselves, so the
|
||||
// lock and modules are honored — the `/jobs/run/preview`-equivalent path.
|
||||
let resp = authed(
|
||||
client().post(format!("{ws}/apps_u/execute_component/{app_path}")),
|
||||
"SECRET_TOKEN_2",
|
||||
)
|
||||
.json(&json!({
|
||||
"args": {},
|
||||
"component": "comp",
|
||||
"raw_code": inline_raw_code(None, false),
|
||||
"force_viewer_static_fields": {}
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
let status = resp.status();
|
||||
let body = resp.text().await?;
|
||||
assert_eq!(status, 200, "preview must be accepted: {body}");
|
||||
let uuid = uuid::Uuid::parse_str(body.trim())?;
|
||||
let (raw_lock, has_modules, _, _, _) = job_fields(&db, uuid).await?;
|
||||
assert_eq!(
|
||||
raw_lock.as_deref(),
|
||||
Some(CALLER_LOCK),
|
||||
"preview must keep the caller-supplied lock"
|
||||
);
|
||||
assert!(has_modules, "preview must keep the caller-supplied modules");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A bare `rawscript/<sha>` policy key (no `<component>:` prefix, as `empty_triggerables`
|
||||
/// migrates v1 policies) matches for any `component`, so run mode must not let a
|
||||
/// path-traversing `component` steer the server-derived `runnable_path`.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_run_mode_rejects_traversal_component(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
let ws = format!("http://localhost:{port}/api/w/test-workspace");
|
||||
|
||||
let app_path = "u/test-user/lockstrip_bare";
|
||||
// Bare key: no `comp:` prefix, so the pin matches regardless of `component`.
|
||||
let resp = authed(client().post(format!("{ws}/apps/create")), "SECRET_TOKEN")
|
||||
.json(&json!({
|
||||
"path": app_path,
|
||||
"summary": "",
|
||||
"value": {},
|
||||
"policy": {
|
||||
"execution_mode": "viewer",
|
||||
"triggerables_v2": { rawscript_pin(CONTENT): { "static_inputs": {}, "one_of_inputs": {} } }
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 201, "create app: {}", resp.text().await?);
|
||||
|
||||
// A component that isn't a single plain segment steers the derived path's
|
||||
// base: separators and `..` traverse, and an empty one shifts it up a level.
|
||||
for bad in ["../../u/attacker/evil", "..", "a/b", ""] {
|
||||
let resp = authed(
|
||||
client().post(format!("{ws}/apps_u/execute_component/{app_path}")),
|
||||
"SECRET_TOKEN_2",
|
||||
)
|
||||
.json(&json!({
|
||||
"args": {},
|
||||
"component": bad,
|
||||
"raw_code": inline_raw_code(None, false)
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
let status = resp.status();
|
||||
let body = resp.text().await?;
|
||||
assert_eq!(
|
||||
status, 400,
|
||||
"run mode must reject component {bad:?}: got {status}: {body}"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1209,6 +1209,100 @@ export function main() {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A deployed flow runs an inline step as the `flow_node` its deploy rewrote it into,
|
||||
/// a `FlowScript` job rather than the preview job the editor runs. A workflow-as-code
|
||||
/// step's `task()` children must dispatch from that kind too, as re-runs of the same
|
||||
/// node, or the step passes its editor test and fails once deployed.
|
||||
///
|
||||
/// The step is cached: a child that shared the parent's result-cache key would hand
|
||||
/// its own result (`10`) back to the parent on resume, in place of the workflow's.
|
||||
#[sqlx::test(fixtures("base", "wac_flow_script"))]
|
||||
async fn test_bun_wac_task_dispatch_from_flow_script(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
use windmill_common::flows::FlowNodeId;
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let node = FlowNodeId(3000000000000011);
|
||||
let job = RunJob::from(JobPayload::FlowScript {
|
||||
id: node,
|
||||
path: "f/system/wac_flow_script/a".to_string(),
|
||||
language: ScriptLang::Bun,
|
||||
cache_ttl: Some(60),
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(),
|
||||
})
|
||||
.arg("n", serde_json::json!(5))
|
||||
.run_until_complete(&db, false, port)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
job.json_result().unwrap(),
|
||||
serde_json::json!({"doubled": 10})
|
||||
);
|
||||
|
||||
let children: Vec<(String, Option<i64>, Option<i32>)> = sqlx::query_as(
|
||||
"SELECT kind::text, runnable_id, cache_ttl FROM v2_job WHERE parent_job = $1",
|
||||
)
|
||||
.bind(job.id)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
children,
|
||||
vec![("flowscript".to_string(), Some(node.0), None)],
|
||||
"the task child re-runs the parent's flow node, outside the result cache"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `task(fn, { cache_ttl })` on an inline task of a deployed flow's step: the child runs
|
||||
/// the parent's code with the parent's arguments, so its result-cache key carries its
|
||||
/// step key, or the parent and every sibling would read its result back as their own.
|
||||
#[sqlx::test(fixtures("base", "wac_flow_script"))]
|
||||
async fn test_bun_wac_inline_task_cache_is_per_task(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
use windmill_common::flows::FlowNodeId;
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let payload = || JobPayload::FlowScript {
|
||||
id: FlowNodeId(3000000000000012),
|
||||
path: "f/system/wac_flow_script/a".to_string(),
|
||||
language: ScriptLang::Bun,
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(),
|
||||
};
|
||||
|
||||
let mut children_from_cache = Vec::new();
|
||||
for _ in 0..2 {
|
||||
let job = RunJob::from(payload())
|
||||
.arg("n", serde_json::json!(5))
|
||||
.run_until_complete(&db, false, port)
|
||||
.await;
|
||||
assert_eq!(
|
||||
job.json_result().unwrap(),
|
||||
serde_json::json!({"doubled": 10, "tripled": 15})
|
||||
);
|
||||
let from_cache: i64 = sqlx::query_scalar(
|
||||
"SELECT count(*) FROM job_logs l JOIN v2_job j ON j.id = l.job_id \
|
||||
WHERE j.parent_job = $1 AND l.logs LIKE '%found in cache%'",
|
||||
)
|
||||
.bind(job.id)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
children_from_cache.push(from_cache);
|
||||
}
|
||||
assert_eq!(
|
||||
children_from_cache,
|
||||
vec![0, 2],
|
||||
"the second run serves each task from its own cache entry"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Environment Variable Tests
|
||||
// ============================================================================
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
-- Layers on `base`: a second workspace the superadmin `test-user` is also a member of, so a
|
||||
-- share can be requested through the wrong workspace's path by a caller the route accepts.
|
||||
|
||||
INSERT INTO workspace (id, name, owner) VALUES
|
||||
('test-workspace-2', 'test-workspace-2', 'test-user');
|
||||
|
||||
INSERT INTO workspace_key(workspace_id, kind, key) VALUES
|
||||
('test-workspace-2', 'cloud', 'test-key-2');
|
||||
|
||||
INSERT INTO workspace_settings (workspace_id) VALUES
|
||||
('test-workspace-2');
|
||||
|
||||
INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES
|
||||
('test-workspace-2', 'all', 'All users', '{}');
|
||||
|
||||
INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES
|
||||
('test-workspace-2', 'test@windmill.dev', 'test-user', true, 'Admin');
|
||||
@@ -0,0 +1,46 @@
|
||||
-- A parent workspace whose auto-pulled repository was last saved by alice, and a fork of
|
||||
-- it holding only carol, the non-admin who created it. aaron is an admin who sorts before
|
||||
-- alice; bob is no longer an admin; dora is a workspace admin deactivated on the instance.
|
||||
-- sam and sue are instance superadmins who are not members: sam's instance username is
|
||||
-- carol's, sue's is unclaimed.
|
||||
|
||||
INSERT INTO workspace (id, name, owner) VALUES ('ap-parent', 'ap-parent', 'alice@windmill.dev');
|
||||
INSERT INTO workspace (id, name, owner, parent_workspace_id)
|
||||
VALUES ('wm-fork-feat', 'feat', 'carol@windmill.dev', 'ap-parent');
|
||||
|
||||
INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES
|
||||
('ap-parent', 'all', 'All users', '{}'),
|
||||
('wm-fork-feat', 'all', 'All users', '{}');
|
||||
|
||||
INSERT INTO password (email, password_hash, login_type, super_admin, verified, name, disabled) VALUES
|
||||
('aaron@windmill.dev', 'not-a-real-hash', 'password', false, true, 'aaron', false),
|
||||
('alice@windmill.dev', 'not-a-real-hash', 'password', false, true, 'alice', false),
|
||||
('bob@windmill.dev', 'not-a-real-hash', 'password', false, true, 'bob', false),
|
||||
('carol@windmill.dev', 'not-a-real-hash', 'password', false, true, 'carol', false),
|
||||
('dora@windmill.dev', 'not-a-real-hash', 'password', false, true, 'dora', true);
|
||||
|
||||
INSERT INTO password (email, password_hash, login_type, super_admin, verified, name, disabled, username) VALUES
|
||||
('sam@windmill.dev', 'not-a-real-hash', 'password', true, true, 'sam', false, 'carol'),
|
||||
('sue@windmill.dev', 'not-a-real-hash', 'password', true, true, 'sue', false, 'sue');
|
||||
|
||||
INSERT INTO usr (workspace_id, email, username, is_admin) VALUES
|
||||
('ap-parent', 'aaron@windmill.dev', 'aaron', true),
|
||||
('ap-parent', 'alice@windmill.dev', 'alice', true),
|
||||
('ap-parent', 'bob@windmill.dev', 'bob', false),
|
||||
('ap-parent', 'carol@windmill.dev', 'carol', false),
|
||||
('ap-parent', 'dora@windmill.dev', 'dora', true),
|
||||
('wm-fork-feat', 'carol@windmill.dev', 'carol', false);
|
||||
|
||||
INSERT INTO resource (workspace_id, path, value, resource_type, extra_perms, created_by) VALUES
|
||||
('ap-parent', 'u/alice/repo', '{"url": "https://github.com/test/repo.git", "branch": "main"}',
|
||||
'git_repository', '{}', 'alice'),
|
||||
('wm-fork-feat', 'u/alice/repo', '{"url": "https://github.com/test/repo.git", "branch": "main"}',
|
||||
'git_repository', '{}', 'alice');
|
||||
|
||||
INSERT INTO workspace_settings (workspace_id, git_sync) VALUES
|
||||
('ap-parent', '{"repositories":[{"git_repo_resource_path":"$res:u/alice/repo",
|
||||
"use_individual_branch":false,"group_by_folder":false,
|
||||
"auto_pull":{"enabled":true,"mode":"polling","sync_forks":true,
|
||||
"enabled_by":"alice@windmill.dev"}}]}'),
|
||||
('wm-fork-feat', '{"repositories":[{"git_repo_resource_path":"$res:u/alice/repo",
|
||||
"use_individual_branch":false,"group_by_folder":false}]}');
|
||||
@@ -0,0 +1,46 @@
|
||||
-- A three-deep fork chain whose schedule rows were cloned down at fork time.
|
||||
-- The middle fork has since deleted its copy, so the leaf's schedule shares
|
||||
-- its cron only with the root — the shape a direct-parent check misses.
|
||||
|
||||
INSERT INTO workspace (id, name, owner, parent_workspace_id) VALUES
|
||||
('sfc-root', 'sfc-root', 'sfc-admin', NULL),
|
||||
('sfc-mid', 'sfc-mid', 'sfc-admin', 'sfc-root'),
|
||||
('sfc-leaf', 'sfc-leaf', 'sfc-admin', 'sfc-mid');
|
||||
|
||||
INSERT INTO workspace_key (workspace_id, kind, key) VALUES
|
||||
('sfc-root', 'cloud', 'sfc-root-key'),
|
||||
('sfc-mid', 'cloud', 'sfc-mid-key'),
|
||||
('sfc-leaf', 'cloud', 'sfc-leaf-key');
|
||||
|
||||
INSERT INTO workspace_settings (workspace_id) VALUES
|
||||
('sfc-root'), ('sfc-mid'), ('sfc-leaf');
|
||||
|
||||
INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES
|
||||
('sfc-root', 'all', 'All users', '{}'),
|
||||
('sfc-mid', 'all', 'All users', '{}'),
|
||||
('sfc-leaf', 'all', 'All users', '{}');
|
||||
|
||||
INSERT INTO password(email, password_hash, login_type, super_admin, verified, name, username)
|
||||
VALUES ('sfc-admin@windmill.dev', 'x', 'password', true, true, 'SFC Admin', 'sfc-admin');
|
||||
|
||||
INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES
|
||||
('sfc-root', 'sfc-admin@windmill.dev', 'sfc-admin', true, 'Admin'),
|
||||
('sfc-mid', 'sfc-admin@windmill.dev', 'sfc-admin', true, 'Admin'),
|
||||
('sfc-leaf', 'sfc-admin@windmill.dev', 'sfc-admin', true, 'Admin');
|
||||
|
||||
INSERT INTO token(token_hash, token_prefix, token, email, label, super_admin)
|
||||
VALUES (encode(sha256('SFC_ADMIN_TOKEN'::bytea), 'hex'), 'SFC_ADMIN_', 'SFC_ADMIN_TOKEN', 'sfc-admin@windmill.dev', 't', true);
|
||||
|
||||
-- Enabling pushes the next run, which needs the scheduled script to exist.
|
||||
INSERT INTO script (workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES
|
||||
('sfc-leaf', 'sfc-admin', 'export async function main() { return "ok" }', '{}', '', '', 'f/shared/job', 7788001, 'deno', '');
|
||||
|
||||
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms, permissioned_as)
|
||||
VALUES
|
||||
('sfc-root', 'f/shared/nightly', 'sfc-admin', NOW(), '0 0 0 * * *', true, 'f/shared/job', '{}', false, 'sfc-admin@windmill.dev', 'UTC', '{}', 'u/sfc-admin'),
|
||||
('sfc-leaf', 'f/shared/nightly', 'sfc-admin', NOW(), '0 0 0 * * *', false, 'f/shared/job', '{}', false, 'sfc-admin@windmill.dev', 'UTC', '{}', 'u/sfc-admin'),
|
||||
-- A path only the leaf has: nothing above shares it.
|
||||
('sfc-leaf', 'f/shared/own', 'sfc-admin', NOW(), '0 0 0 * * *', false, 'f/shared/job', '{}', false, 'sfc-admin@windmill.dev', 'UTC', '{}', 'u/sfc-admin');
|
||||
|
||||
GRANT ALL PRIVILEGES ON TABLE workspace_key TO windmill_admin;
|
||||
GRANT ALL PRIVILEGES ON TABLE workspace_key TO windmill_user;
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
-- A deployed flow whose inline bun step is workflow-as-code calling task(), in the
|
||||
-- shape the deploy leaves behind: the RawScript module rewritten into a flow_node that
|
||||
-- the step then runs as a FlowScript job. No lock, so the worker resolves
|
||||
-- windmill-client at run time like the other bun fixtures.
|
||||
INSERT INTO public.flow(workspace_id, summary, description, path, versions, schema, value, edited_by) VALUES (
|
||||
'test-workspace', '', '',
|
||||
'f/system/wac_flow_script',
|
||||
'{}',
|
||||
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"n":{"type":"integer","description":""}},"required":[],"type":"object"}',
|
||||
'{"modules":[{"id":"a","value":{"type":"flowscript","id":3000000000000011,"language":"bun","input_transforms":{"n":{"expr":"flow_input.n","type":"javascript"}}}}]}',
|
||||
'system'
|
||||
);
|
||||
|
||||
INSERT INTO public.flow_node(id, workspace_id, path, hash_v2, lock, code) VALUES (
|
||||
3000000000000011,
|
||||
'test-workspace',
|
||||
'f/system/wac_flow_script',
|
||||
'0000000000000000000000000000000000000000000000000000000000000011',
|
||||
NULL,
|
||||
E'import { workflow, task } from "windmill-client";
|
||||
|
||||
const double = task(async (n: number) => {
|
||||
return n * 2;
|
||||
});
|
||||
|
||||
export const main = workflow(async (n: number) => {
|
||||
const d = await double(n);
|
||||
return { doubled: d };
|
||||
});'
|
||||
);
|
||||
|
||||
-- The same flow's step with two tasks that cache their own result.
|
||||
INSERT INTO public.flow_node(id, workspace_id, path, hash_v2, lock, code) VALUES (
|
||||
3000000000000012,
|
||||
'test-workspace',
|
||||
'f/system/wac_flow_script',
|
||||
'0000000000000000000000000000000000000000000000000000000000000012',
|
||||
NULL,
|
||||
E'import { workflow, task } from "windmill-client";
|
||||
|
||||
const double = task(async (n: number) => {
|
||||
return n * 2;
|
||||
}, { cache_ttl: 60 });
|
||||
const triple = task(async (n: number) => {
|
||||
return n * 3;
|
||||
}, { cache_ttl: 60 });
|
||||
|
||||
export const main = workflow(async (n: number) => {
|
||||
const d = await double(n);
|
||||
const t = await triple(n);
|
||||
return { doubled: d, tripled: t };
|
||||
});'
|
||||
);
|
||||
@@ -0,0 +1,194 @@
|
||||
//! An automatic pull runs as the admin stamped on the repository's settings, never as
|
||||
//! someone picked from the workspace, and stops once that admin is revoked. A fork's
|
||||
//! pull runs as the parent's pull identity, added to the fork first.
|
||||
#![cfg(all(feature = "enterprise", feature = "private"))]
|
||||
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_common::workspaces::GitRepositorySettings;
|
||||
use windmill_git_sync::{reconcile_and_enqueue_pull, reconcile_fork_branch_pull};
|
||||
|
||||
const PARENT: &str = "ap-parent";
|
||||
const FORK: &str = "wm-fork-feat";
|
||||
const REPO: &str = "$res:u/alice/repo";
|
||||
|
||||
fn repo_enabled_by(email: &str) -> GitRepositorySettings {
|
||||
serde_json::from_value(serde_json::json!({
|
||||
"git_repo_resource_path": REPO,
|
||||
"use_individual_branch": false,
|
||||
"group_by_folder": false,
|
||||
"auto_pull": { "enabled": true, "enabled_by": email }
|
||||
}))
|
||||
.expect("repository settings")
|
||||
}
|
||||
|
||||
/// `(created_by, permissioned_as, permissioned_as_email)` of every pull job in `w_id`.
|
||||
async fn pull_identities(
|
||||
db: &Pool<Postgres>,
|
||||
w_id: &str,
|
||||
) -> anyhow::Result<Vec<(String, String, Option<String>)>> {
|
||||
Ok(sqlx::query_as(
|
||||
"SELECT created_by, permissioned_as, permissioned_as_email FROM v2_job \
|
||||
WHERE workspace_id = $1 AND kind = 'deploymentcallback'",
|
||||
)
|
||||
.bind(w_id)
|
||||
.fetch_all(db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
fn identity(username: &str) -> (String, String, Option<String>) {
|
||||
(
|
||||
username.to_string(),
|
||||
format!("u/{username}"),
|
||||
Some(format!("{username}@windmill.dev")),
|
||||
)
|
||||
}
|
||||
|
||||
async fn recorded_pull_error(db: &Pool<Postgres>, w_id: &str) -> anyhow::Result<String> {
|
||||
let git_sync: serde_json::Value =
|
||||
sqlx::query_scalar("SELECT git_sync FROM workspace_settings WHERE workspace_id = $1")
|
||||
.bind(w_id)
|
||||
.fetch_one(db)
|
||||
.await?;
|
||||
Ok(
|
||||
git_sync["repositories"][0]["auto_pull"]["last_pull_status"]["error"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("git_sync_autopull_identity"))]
|
||||
async fn pull_runs_as_the_admin_who_enabled_it(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
let job = reconcile_and_enqueue_pull(
|
||||
&db,
|
||||
PARENT,
|
||||
&repo_enabled_by("alice@windmill.dev"),
|
||||
"main",
|
||||
"abc123",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert!(job.is_some());
|
||||
assert_eq!(pull_identities(&db, PARENT).await?, vec![identity("alice")]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// bob was demoted in the workspace; dora is still a workspace admin but deactivated on
|
||||
/// the instance. Neither may run the pull, and the failure lands on the status rather
|
||||
/// than as an error, which a webhook delivery would turn into a failed response.
|
||||
#[sqlx::test(fixtures("git_sync_autopull_identity"))]
|
||||
async fn pull_fails_on_the_status_once_the_enabling_admin_is_revoked(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
for email in ["bob@windmill.dev", "dora@windmill.dev"] {
|
||||
let job = reconcile_and_enqueue_pull(
|
||||
&db,
|
||||
PARENT,
|
||||
&repo_enabled_by(email),
|
||||
"main",
|
||||
"abc123",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
assert!(job.is_none(), "{email} must not run the pull");
|
||||
let error = recorded_pull_error(&db, PARENT).await?;
|
||||
assert!(error.contains(email), "{error}");
|
||||
}
|
||||
assert!(pull_identities(&db, PARENT).await?.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("git_sync_autopull_identity"))]
|
||||
async fn fork_pull_runs_as_the_parent_admin_added_to_the_fork(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
let job = reconcile_fork_branch_pull(&db, PARENT, REPO, "wm-fork/main/feat", "main", "abc123")
|
||||
.await?;
|
||||
assert!(
|
||||
job.is_some(),
|
||||
"the fork branch must route to the fork and enqueue"
|
||||
);
|
||||
|
||||
let (is_admin, in_all): (bool, bool) = sqlx::query_as(
|
||||
"SELECT u.is_admin, EXISTS (SELECT 1 FROM usr_to_group g \
|
||||
WHERE g.workspace_id = u.workspace_id AND g.usr = u.username AND g.group_ = 'all') \
|
||||
FROM usr u WHERE u.workspace_id = $1 AND u.email = 'alice@windmill.dev'",
|
||||
)
|
||||
.bind(FORK)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
assert!(
|
||||
is_admin && in_all,
|
||||
"alice must be an admin member of the fork"
|
||||
);
|
||||
assert_eq!(pull_identities(&db, FORK).await?, vec![identity("alice")]);
|
||||
|
||||
let grants: i64 = sqlx::query_scalar(
|
||||
"SELECT count(*) FROM audit_partitioned WHERE workspace_id = $1 \
|
||||
AND operation = 'users.git_sync_fork_add' AND resource = 'alice@windmill.dev'",
|
||||
)
|
||||
.bind(FORK)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
assert_eq!(grants, 1, "adding alice to the fork must be audited");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A superadmin who is not a member runs the pull under their instance username, and
|
||||
/// `u/<username>` resolves through the workspace's members first. sam's instance username
|
||||
/// is carol's, so sam's stamp must not run the pull as carol; sue's is unclaimed.
|
||||
#[sqlx::test(fixtures("git_sync_autopull_identity"))]
|
||||
async fn a_non_member_superadmin_runs_the_pull_only_under_an_unclaimed_username(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
let job = reconcile_and_enqueue_pull(
|
||||
&db,
|
||||
PARENT,
|
||||
&repo_enabled_by("sam@windmill.dev"),
|
||||
"main",
|
||||
"abc123",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
assert!(job.is_none(), "sam's username belongs to carol");
|
||||
assert!(pull_identities(&db, PARENT).await?.is_empty());
|
||||
|
||||
let job = reconcile_and_enqueue_pull(
|
||||
&db,
|
||||
PARENT,
|
||||
&repo_enabled_by("sue@windmill.dev"),
|
||||
"main",
|
||||
"abc123",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
assert!(job.is_some());
|
||||
assert_eq!(pull_identities(&db, PARENT).await?, vec![identity("sue")]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// With no stamp on the parent, the fork pull still runs as the parent's first active
|
||||
/// admin: the fork holds only its non-admin creator, so no identity resolved in the fork
|
||||
/// could run it.
|
||||
#[sqlx::test(fixtures("git_sync_autopull_identity"))]
|
||||
async fn unstamped_fork_pull_runs_as_the_parents_first_admin(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
sqlx::query(
|
||||
"UPDATE workspace_settings SET git_sync = git_sync #- '{repositories,0,auto_pull,enabled_by}' \
|
||||
WHERE workspace_id = $1",
|
||||
)
|
||||
.bind(PARENT)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
let job = reconcile_fork_branch_pull(&db, PARENT, REPO, "wm-fork/main/feat", "main", "abc123")
|
||||
.await?;
|
||||
assert!(
|
||||
job.is_some(),
|
||||
"an unstamped parent must still sync its forks"
|
||||
);
|
||||
assert_eq!(pull_identities(&db, FORK).await?, vec![identity("aaron")]);
|
||||
Ok(())
|
||||
}
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_common::git_sync_ee::{
|
||||
create_repo_webhook, git_credential_for_url, repo_provider, repo_supports_managed_git_features,
|
||||
set_git_credential, GitProvider,
|
||||
create_repo_webhook, git_app_installations_for, git_credential_for_url, managed_pr_base_branch,
|
||||
repo_provider, repo_supports_managed_git_features, set_git_credential, GitProvider,
|
||||
};
|
||||
use windmill_common::workspaces::GitCredentialProvider;
|
||||
|
||||
@@ -278,3 +278,99 @@ async fn an_unreachable_gitlab_host_is_the_reported_error(
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// GitHub App installations are normally copied into a fork, but a workspace
|
||||
/// attached as a dev workspace, or forked before its parent connected the App,
|
||||
/// holds none, and neither does anything forked from it. The lookup reaches the
|
||||
/// nearest workspace up the chain that holds some, and the background App path
|
||||
/// (PR base resolution here) authenticates with that installation's token.
|
||||
#[sqlx::test(fixtures("git_sync_fork_credential"))]
|
||||
async fn app_installations_come_from_the_nearest_ancestor_holding_some(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
use axum::{routing::get, Router};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
// A stand-in GitHub API: one repository, and a record of who asked for it.
|
||||
let seen: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
let app = Router::new().route(
|
||||
"/api/v3/repos/acme/repo",
|
||||
get({
|
||||
let seen = seen.clone();
|
||||
move |headers: axum::http::HeaderMap| {
|
||||
let seen = seen.clone();
|
||||
async move {
|
||||
let auth = headers
|
||||
.get("authorization")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
seen.lock().unwrap().push(auth);
|
||||
axum::Json(serde_json::json!({ "default_branch": "trunk" }))
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
|
||||
let port = listener.local_addr()?.port();
|
||||
tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
|
||||
let stub = format!("http://127.0.0.1:{port}");
|
||||
|
||||
// The root holds the installation, with a cached token so nothing is minted.
|
||||
sqlx::query(
|
||||
"UPDATE workspace_settings SET git_app_installations = $1::jsonb WHERE workspace_id = 'parent-ws'",
|
||||
)
|
||||
.bind(serde_json::json!([{
|
||||
"installation_id": 42, "account_id": "acme", "jwt_token": "x",
|
||||
"github_base_url": stub,
|
||||
"installation_token": "root-token", "installation_token_expiration": 4102444800i64
|
||||
}]))
|
||||
.execute(&db)
|
||||
.await?;
|
||||
// The fork's copy of the resource names the App-backed repository.
|
||||
sqlx::query("UPDATE resource SET value = $1::jsonb WHERE workspace_id = 'deep-fork-ws' AND path = 'u/admin/repo'")
|
||||
.bind(serde_json::json!({ "url": format!("{stub}/acme/repo.git"), "is_github_app": true }))
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
git_app_installations_for(&db, "deep-fork-ws").await?,
|
||||
("parent-ws".to_string(), vec![(42, Some(stub.clone()))]),
|
||||
"two levels down, the root's installations are the ones to use"
|
||||
);
|
||||
assert_eq!(
|
||||
git_app_installations_for(&db, "orphan-ws").await?,
|
||||
("orphan-ws".to_string(), vec![]),
|
||||
"a workspace with nothing above it resolves nothing"
|
||||
);
|
||||
assert_eq!(
|
||||
managed_pr_base_branch(&db, "deep-fork-ws", REPO)
|
||||
.await?
|
||||
.as_deref(),
|
||||
Some("trunk"),
|
||||
"the background App path reaches the repository through the root's installation"
|
||||
);
|
||||
let seen = seen.lock().unwrap().clone();
|
||||
assert!(
|
||||
!seen.is_empty() && seen.iter().all(|auth| auth == "Bearer root-token"),
|
||||
"every call authenticated with the root's cached token: {seen:?}"
|
||||
);
|
||||
|
||||
// A closer holder takes precedence over the root.
|
||||
sqlx::query(
|
||||
"UPDATE workspace_settings SET git_app_installations = $1::jsonb WHERE workspace_id = 'fork-ws'",
|
||||
)
|
||||
.bind(serde_json::json!([{
|
||||
"installation_id": 7, "account_id": "acme", "jwt_token": "x",
|
||||
"github_base_url": stub,
|
||||
"installation_token": "mid-token", "installation_token_expiration": 4102444800i64
|
||||
}]))
|
||||
.execute(&db)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
git_app_installations_for(&db, "deep-fork-ws").await?.0,
|
||||
"fork-ws",
|
||||
"the nearest holder wins over the root"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
//! Enabling a schedule in a fork warns about every ancestor sharing the path,
|
||||
//! not only the direct parent: the row was cloned down the whole chain, so the
|
||||
//! cron is shared with whichever ancestors still hold a copy.
|
||||
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
use windmill_test_utils::*;
|
||||
|
||||
async fn set_enabled(
|
||||
base: &str,
|
||||
path: &str,
|
||||
enabled: bool,
|
||||
force: bool,
|
||||
) -> anyhow::Result<(u16, String)> {
|
||||
let resp = reqwest::Client::new()
|
||||
.post(format!("{base}/api/w/sfc-leaf/schedules/setenabled/{path}"))
|
||||
.header("Authorization", "Bearer SFC_ADMIN_TOKEN")
|
||||
.json(&json!({ "enabled": enabled, "force": force }))
|
||||
.send()
|
||||
.await?;
|
||||
Ok((resp.status().as_u16(), resp.text().await?))
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("schedule_fork_conflict"))]
|
||||
async fn enabling_in_a_fork_names_the_nearest_ancestor_sharing_the_path(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let base = format!("http://localhost:{}", server.addr.port());
|
||||
|
||||
let (status, body) = set_enabled(&base, "f/shared/nightly", true, false).await?;
|
||||
assert_eq!(status, 400, "{body}");
|
||||
assert!(
|
||||
body.contains("fork-conflict:schedule:sfc-root"),
|
||||
"the middle fork deleted its copy, so the root is the one still sharing the cron: {body}"
|
||||
);
|
||||
|
||||
let (status, body) = set_enabled(&base, "f/shared/own", true, false).await?;
|
||||
assert_eq!(
|
||||
status, 200,
|
||||
"a path nothing upstream has enables freely: {body}"
|
||||
);
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms, permissioned_as)
|
||||
VALUES ('sfc-mid', 'f/shared/nightly', 'sfc-admin', NOW(), '0 0 0 * * *', false, 'f/shared/job', '{}', false, 'sfc-admin@windmill.dev', 'UTC', '{}', 'u/sfc-admin')",
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
let (status, body) = set_enabled(&base, "f/shared/nightly", true, false).await?;
|
||||
assert_eq!(status, 400, "{body}");
|
||||
assert!(
|
||||
body.contains("fork-conflict:schedule:sfc-mid"),
|
||||
"with the parent holding a copy again, it is the nearest and gets named: {body}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -3,18 +3,23 @@
|
||||
//! extractor actually grants. Membership is not the only path: a superadmin is authed into
|
||||
//! any existing workspace without a `usr` row, and `admins` has no `usr` rows at all, so
|
||||
//! answering from `usr` alone reports live workspaces as unresolvable and the client deletes
|
||||
//! sessions that still work.
|
||||
//! sessions that still work. `POST /workspaces/session_workspace_retention`, the AI session
|
||||
//! retention the same client deletes its own copies by, is a workspace setting and answers to
|
||||
//! the stricter bar, which is why the two are separate routes and tested together.
|
||||
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use std::collections::HashMap;
|
||||
use windmill_test_utils::*;
|
||||
|
||||
async fn status(port: u16, token: &str, ids: &[&str]) -> anyhow::Result<HashMap<String, String>> {
|
||||
async fn post<T: serde::de::DeserializeOwned>(
|
||||
port: u16,
|
||||
route: &str,
|
||||
token: &str,
|
||||
ids: &[&str],
|
||||
) -> anyhow::Result<T> {
|
||||
let resp = reqwest::Client::new()
|
||||
.post(format!(
|
||||
"http://localhost:{port}/api/workspaces/session_workspace_status"
|
||||
))
|
||||
.post(format!("http://localhost:{port}/api/workspaces/{route}"))
|
||||
.header("Authorization", format!("Bearer {token}"))
|
||||
.json(&json!({ "workspace_ids": ids }))
|
||||
.send()
|
||||
@@ -23,6 +28,14 @@ async fn status(port: u16, token: &str, ids: &[&str]) -> anyhow::Result<HashMap<
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
async fn status(port: u16, token: &str, ids: &[&str]) -> anyhow::Result<HashMap<String, String>> {
|
||||
post(port, "session_workspace_status", token, ids).await
|
||||
}
|
||||
|
||||
async fn retention(port: u16, token: &str, ids: &[&str]) -> anyhow::Result<HashMap<String, u32>> {
|
||||
post(port, "session_workspace_retention", token, ids).await
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base", "session_workspace_status"))]
|
||||
async fn test_superadmin_reaches_workspaces_without_a_usr_row(
|
||||
db: Pool<Postgres>,
|
||||
@@ -60,3 +73,48 @@ async fn test_superadmin_reaches_workspaces_without_a_usr_row(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The retention a browser deletes its own copies by is a workspace setting, so unlike the
|
||||
/// status it is told only to a caller the authed extractor would let in.
|
||||
#[sqlx::test(fixtures("base", "session_workspace_status"))]
|
||||
async fn test_session_retention_is_told_only_to_members_who_can_be_authed(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let ids = ["foreign-workspace", "test-workspace", "no-such-workspace"];
|
||||
sqlx::query(
|
||||
"UPDATE workspace_settings SET ai_config = '{\"sessions_retention_days\": 7}' \
|
||||
WHERE workspace_id IN ('test-workspace', 'foreign-workspace')",
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
// test@windmill.dev is a superadmin: authed into every workspace that exists.
|
||||
let sa = retention(port, "SECRET_TOKEN", &ids).await?;
|
||||
assert_eq!(sa["test-workspace"], 7);
|
||||
assert_eq!(sa["foreign-workspace"], 7);
|
||||
assert!(!sa.contains_key("no-such-workspace"));
|
||||
|
||||
// test2@windmill.dev is a member of test-workspace only.
|
||||
let usr = retention(port, "SECRET_TOKEN_2", &ids).await?;
|
||||
assert_eq!(usr["test-workspace"], 7);
|
||||
assert!(!usr.contains_key("foreign-workspace"));
|
||||
|
||||
// A disabled membership still reconciles its sessions — the status stays `active` — but
|
||||
// cannot be authed into the workspace, so it is told no setting.
|
||||
sqlx::query("UPDATE usr SET disabled = true WHERE workspace_id = 'test-workspace'")
|
||||
.execute(&db)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
status(port, "SECRET_TOKEN_2", &ids).await?["test-workspace"],
|
||||
"active"
|
||||
);
|
||||
assert!(!retention(port, "SECRET_TOKEN_2", &ids)
|
||||
.await?
|
||||
.contains_key("test-workspace"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
//! A WAC v2 parent parks on its dispatched children and is woken by their
|
||||
//! completions. A child does not always complete through the worker that ran it:
|
||||
//! the zombie monitor and a force cancel both go straight to
|
||||
//! `add_completed_job_error`. The parent must be woken from there too, or it sits
|
||||
//! out its whole suspend window and then runs the task a second time.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use sqlx::{types::Json, Pool, Postgres};
|
||||
use uuid::Uuid;
|
||||
use windmill_queue::{add_completed_job, add_completed_job_error, get_mini_completed_job};
|
||||
|
||||
const W_ID: &str = "test-workspace";
|
||||
|
||||
async fn insert_job(db: &Pool<Postgres>, id: Uuid, parent: Option<Uuid>) -> anyhow::Result<()> {
|
||||
sqlx::query(
|
||||
"INSERT INTO v2_job (id, workspace_id, created_by, created_at, permissioned_as, \
|
||||
permissioned_as_email, kind, script_lang, runnable_path, tag, visible_to_owner, parent_job) \
|
||||
VALUES ($1, $2, 'test-user', now(), 'u/test-user', 'test@windmill.dev', \
|
||||
'script', 'bun', 'u/test-user/wac', 'bun', true, $3)",
|
||||
)
|
||||
.bind(id)
|
||||
.bind(W_ID)
|
||||
.bind(parent)
|
||||
.execute(db)
|
||||
.await?;
|
||||
sqlx::query(
|
||||
"INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, running, tag) \
|
||||
VALUES ($1, $2, now(), true, 'bun')",
|
||||
)
|
||||
.bind(id)
|
||||
.bind(W_ID)
|
||||
.execute(db)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A parent parked on `steps` (step key → child job), the shape
|
||||
/// `handle_wac_v2_output` leaves behind once the children are pushed.
|
||||
async fn plant_parked_parent(db: &Pool<Postgres>, steps: &[(&str, Uuid)]) -> anyhow::Result<Uuid> {
|
||||
let parent = Uuid::new_v4();
|
||||
insert_job(db, parent, None).await?;
|
||||
sqlx::query(
|
||||
"UPDATE v2_job_queue SET suspend = $2, suspend_until = now() + interval '14 days' \
|
||||
WHERE id = $1",
|
||||
)
|
||||
.bind(parent)
|
||||
.bind(steps.len() as i32)
|
||||
.execute(db)
|
||||
.await?;
|
||||
let job_ids: serde_json::Map<String, Value> = steps
|
||||
.iter()
|
||||
.map(|(k, id)| (k.to_string(), json!(id.to_string())))
|
||||
.collect();
|
||||
let keys: Vec<&str> = steps.iter().map(|(k, _)| *k).collect();
|
||||
sqlx::query("INSERT INTO v2_job_status (id, workflow_as_code_status) VALUES ($1, $2)")
|
||||
.bind(parent)
|
||||
.bind(json!({
|
||||
"_checkpoint": {
|
||||
"completed_steps": {},
|
||||
"pending_steps": { "mode": "dispatch", "keys": keys, "job_ids": job_ids },
|
||||
"job_ids": job_ids,
|
||||
}
|
||||
}))
|
||||
.execute(db)
|
||||
.await?;
|
||||
for (_, child) in steps {
|
||||
insert_job(db, *child, Some(parent)).await?;
|
||||
}
|
||||
Ok(parent)
|
||||
}
|
||||
|
||||
async fn parent_state(db: &Pool<Postgres>, parent: Uuid) -> anyhow::Result<(i32, bool, Value)> {
|
||||
let (suspend, parked, status): (i32, bool, Value) = sqlx::query_as(
|
||||
"SELECT q.suspend, q.suspend_until IS NOT NULL, s.workflow_as_code_status \
|
||||
FROM v2_job_queue q JOIN v2_job_status s USING (id) WHERE q.id = $1",
|
||||
)
|
||||
.bind(parent)
|
||||
.fetch_one(db)
|
||||
.await?;
|
||||
Ok((suspend, parked, status))
|
||||
}
|
||||
|
||||
/// The zombie monitor's path: `handle_job_error` → `add_completed_job_error`, never
|
||||
/// the worker's result processor. The parent must come out of it pullable, with the
|
||||
/// failure recorded under the step so the workflow's `try/catch` sees a task error.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn a_child_failed_outside_the_worker_wakes_its_parent(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
let child = Uuid::new_v4();
|
||||
let parent = plant_parked_parent(&db, &[("slowTask", child)]).await?;
|
||||
let child_job = get_mini_completed_job(&child, W_ID, &db).await?.unwrap();
|
||||
|
||||
add_completed_job_error(
|
||||
&db,
|
||||
&child_job,
|
||||
0,
|
||||
None,
|
||||
json!({"name": "ExecutionErr", "message": "Job timed out after no ping"}),
|
||||
"monitor",
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (suspend, parked, status) = parent_state(&db, parent).await?;
|
||||
assert_eq!(suspend, 0, "the parent must be released");
|
||||
assert!(
|
||||
parked,
|
||||
"suspend_until stays set: the suspended pull query keys on it"
|
||||
);
|
||||
let step = &status["_checkpoint"]["completed_steps"]["slowTask"];
|
||||
assert_eq!(step["__wmill_error"], json!(true), "{status}");
|
||||
assert_eq!(step["child_job_id"], json!(child.to_string()));
|
||||
assert_eq!(
|
||||
step["result"]["error"]["message"],
|
||||
json!("Job timed out after no ping")
|
||||
);
|
||||
assert!(
|
||||
status["_checkpoint"].get("pending_steps").is_none(),
|
||||
"nothing left to wait on: {status}"
|
||||
);
|
||||
assert!(
|
||||
windmill_common::wac::WAC_SUSPEND_READY.swap(false, std::sync::atomic::Ordering::Relaxed)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Only a child the parent is waiting on moves the counter. A child the body
|
||||
/// launched itself, or a completion arriving after the key was re-dispatched to
|
||||
/// another job, records its timeline entry and nothing else.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn a_child_the_parent_is_not_waiting_on_leaves_it_parked(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
let awaited = Uuid::new_v4();
|
||||
let parent = plant_parked_parent(&db, &[("task", awaited)]).await?;
|
||||
let stray = Uuid::new_v4();
|
||||
insert_job(&db, stray, Some(parent)).await?;
|
||||
|
||||
let stray_job = get_mini_completed_job(&stray, W_ID, &db).await?.unwrap();
|
||||
add_completed_job_error(
|
||||
&db,
|
||||
&stray_job,
|
||||
0,
|
||||
None,
|
||||
json!({"message": "boom"}),
|
||||
"w",
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (suspend, _, status) = parent_state(&db, parent).await?;
|
||||
assert_eq!(
|
||||
suspend, 1,
|
||||
"a stray child must not release the parent: {status}"
|
||||
);
|
||||
assert_eq!(status["_checkpoint"]["completed_steps"], json!({}));
|
||||
assert!(
|
||||
status[stray.to_string()]["duration_ms"].is_number(),
|
||||
"the timeline entry is still stamped: {status}"
|
||||
);
|
||||
|
||||
let awaited_job = get_mini_completed_job(&awaited, W_ID, &db).await?.unwrap();
|
||||
let result = serde_json::value::to_raw_value(&json!("done"))?;
|
||||
add_completed_job(
|
||||
&db,
|
||||
&awaited_job,
|
||||
true,
|
||||
false,
|
||||
Json(&result),
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (suspend, _, status) = parent_state(&db, parent).await?;
|
||||
assert_eq!(suspend, 0);
|
||||
assert_eq!(
|
||||
status["_checkpoint"]["completed_steps"]["task"],
|
||||
json!("done")
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -5649,6 +5649,83 @@ async fn test_whileloop_propagates_inner_iterator_eval_failure(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "quickjs", feature = "python"))]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_whileloop_skip_if_evaluated_once_at_entry(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
// Regression test for #11007: `skip_if` on a while-loop module must be
|
||||
// evaluated once, at loop entry, using the preceding step's result.
|
||||
// Re-evaluating it on every iteration aliases `results.first` to the
|
||||
// previous iteration's own result instead, which here lacks `.ok` and
|
||||
// makes `skip_if` incorrectly turn true after the first iteration.
|
||||
let port = 123;
|
||||
let flow: FlowValue = serde_json::from_value(serde_json::json!({
|
||||
"modules": [
|
||||
{
|
||||
"id": "first",
|
||||
"value": {
|
||||
"type": "rawscript",
|
||||
"language": "python3",
|
||||
"content": "def main(): return {\"ok\": True}",
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "outer",
|
||||
"value": {
|
||||
"type": "whileloopflow",
|
||||
"skip_failures": false,
|
||||
"modules": [
|
||||
{
|
||||
"id": "inner",
|
||||
"value": {
|
||||
"input_transforms": {
|
||||
"i": {
|
||||
"type": "javascript",
|
||||
"expr": "flow_input.iter.index",
|
||||
},
|
||||
},
|
||||
"type": "rawscript",
|
||||
"language": "python3",
|
||||
"content": "def main(i): return i",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"skip_if": { "expr": "!results.first.ok" },
|
||||
"stop_after_if": {
|
||||
"expr": "result >= 2",
|
||||
"skip_if_stopped": false,
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
.unwrap();
|
||||
let job = JobPayload::RawFlow { value: flow, path: None, restarted_from: None };
|
||||
|
||||
let cjob = RunJob::from(job).run_until_complete(&db, false, port).await;
|
||||
|
||||
assert!(cjob.success, "flow should succeed");
|
||||
|
||||
let outer_module = get_module(&cjob, "outer").expect("outer module status");
|
||||
match outer_module {
|
||||
windmill_common::flow_status::FlowStatusModule::Success { skipped, flow_jobs, .. } => {
|
||||
assert!(
|
||||
!skipped,
|
||||
"while-loop must not be skipped: skip_if should only run once, at entry"
|
||||
);
|
||||
assert_eq!(
|
||||
flow_jobs.map(|v| v.len()),
|
||||
Some(3),
|
||||
"while-loop should run 3 iterations before stop_after_if halts it"
|
||||
);
|
||||
}
|
||||
other => panic!("expected outer module to be Success, got {other:?}"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "quickjs", feature = "python"))]
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_stop_after_all_iters_if_bad_expr_parallel_branchall(
|
||||
|
||||
@@ -767,6 +767,7 @@ impl QueryBuilder for AnthropicQueryBuilder {
|
||||
|
||||
let AnthropicSSEParser {
|
||||
accumulated_content,
|
||||
accumulated_reasoning,
|
||||
accumulated_tool_calls,
|
||||
events_str,
|
||||
annotations,
|
||||
@@ -790,6 +791,7 @@ impl QueryBuilder for AnthropicQueryBuilder {
|
||||
} else {
|
||||
Some(accumulated_content)
|
||||
},
|
||||
reasoning: (!accumulated_reasoning.is_empty()).then_some(accumulated_reasoning),
|
||||
tool_calls: accumulated_tool_calls.into_values().collect(),
|
||||
events_str: Some(events_str),
|
||||
annotations,
|
||||
@@ -900,6 +902,7 @@ mod tests {
|
||||
attachments: None,
|
||||
has_websearch: false,
|
||||
prompt_cache_key: None,
|
||||
reasoning_summary: false,
|
||||
};
|
||||
|
||||
AnthropicQueryBuilder::new(AIProvider::Anthropic, platform)
|
||||
|
||||
@@ -1188,6 +1188,13 @@ impl BedrockQueryBuilder {
|
||||
Some(accumulated_text)
|
||||
};
|
||||
|
||||
// The block folded for replay is also what the reader sees as thinking. Read out
|
||||
// before the block itself moves into the tool calls below.
|
||||
let reasoning_text = reasoning
|
||||
.as_ref()
|
||||
.and_then(|r| r.reasoning_text.clone())
|
||||
.filter(|t| !t.is_empty());
|
||||
|
||||
let tool_calls = streaming_tool_calls_to_openai(
|
||||
accumulated_tool_calls.into_values().collect(),
|
||||
reasoning,
|
||||
@@ -1195,6 +1202,7 @@ impl BedrockQueryBuilder {
|
||||
|
||||
Ok(ParsedResponse::Text {
|
||||
content,
|
||||
reasoning: reasoning_text,
|
||||
tool_calls,
|
||||
events_str: if events_str.is_empty() {
|
||||
None
|
||||
|
||||
@@ -666,6 +666,7 @@ impl QueryBuilder for GoogleAIQueryBuilder {
|
||||
|
||||
let GeminiSSEParser {
|
||||
accumulated_content,
|
||||
accumulated_reasoning,
|
||||
accumulated_tool_calls,
|
||||
mut events_str,
|
||||
stream_event_processor,
|
||||
@@ -698,6 +699,7 @@ impl QueryBuilder for GoogleAIQueryBuilder {
|
||||
} else {
|
||||
Some(accumulated_content)
|
||||
},
|
||||
reasoning: (!accumulated_reasoning.is_empty()).then_some(accumulated_reasoning),
|
||||
tool_calls: accumulated_tool_calls.into_values().collect(),
|
||||
events_str: Some(events_str),
|
||||
annotations,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
ai_providers::AIProvider,
|
||||
ai_types::OpenAIToolCall,
|
||||
credentials::ProviderCredentials,
|
||||
image_handler::{prepare_messages_for_api, s3_object_to_content_part},
|
||||
proxy::{build_openai_compatible_proxy_request, ProxyBuildArgs, ProxyRequest},
|
||||
query_builder::{BuildRequestArgs, ParsedResponse, QueryBuilder, StreamEventSink},
|
||||
@@ -11,7 +12,14 @@ use crate::{
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use windmill_common::{client::AuthedClient, error::Error};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use windmill_common::{cache::Cache, client::AuthedClient, error::Error};
|
||||
|
||||
use super::REASONING_OFF_SENTINEL;
|
||||
|
||||
// Responses API structures
|
||||
#[derive(Deserialize)]
|
||||
@@ -192,13 +200,79 @@ pub struct ResponsesApiTextFormat {
|
||||
pub format: ResponsesApiTextFormatConfig,
|
||||
}
|
||||
|
||||
/// Reasoning config for the Responses API (`reasoning: { effort }`).
|
||||
/// The summary is intentionally not requested, mirroring the copilot chat: OpenAI
|
||||
/// gates reasoning summaries behind organization verification, so asking for one
|
||||
/// would fail the request for unverified orgs.
|
||||
/// Reasoning config for the Responses API (`reasoning: { effort, summary }`).
|
||||
#[derive(Serialize)]
|
||||
pub struct ResponsesApiReasoning {
|
||||
pub effort: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub summary: Option<String>,
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
/// Refused reasoning summaries, so later requests skip asking instead of paying a
|
||||
/// rejected call each. A refusal belongs to the organization the request bills or to the
|
||||
/// model, so the key holds the model and everything that authenticates (the API key and
|
||||
/// the resource headers, which can carry it instead). Entries expire as an org gets verified.
|
||||
static ref REASONING_SUMMARY_UNAVAILABLE: Cache<(String, u64), Instant> = Cache::new(500);
|
||||
}
|
||||
|
||||
const REASONING_SUMMARY_UNAVAILABLE_TTL: Duration = Duration::from_secs(3600);
|
||||
|
||||
fn reasoning_summary_cache_key(
|
||||
base_url: &str,
|
||||
model: &str,
|
||||
api_key: Option<&str>,
|
||||
custom_headers: &HashMap<String, String>,
|
||||
) -> (String, u64) {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
model.hash(&mut hasher);
|
||||
api_key.hash(&mut hasher);
|
||||
// Sorted: two maps with the same entries can iterate them in different orders.
|
||||
custom_headers
|
||||
.iter()
|
||||
.collect::<BTreeMap<_, _>>()
|
||||
.hash(&mut hasher);
|
||||
(base_url.to_string(), hasher.finish())
|
||||
}
|
||||
|
||||
fn credentials_cache_key(credentials: &ProviderCredentials, model: &str) -> (String, u64) {
|
||||
reasoning_summary_cache_key(
|
||||
&credentials.base_url,
|
||||
model,
|
||||
credentials.api_key.as_deref(),
|
||||
&credentials.custom_headers,
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether this model is known to be refused reasoning summaries with these credentials.
|
||||
pub fn is_reasoning_summary_unavailable(credentials: &ProviderCredentials, model: &str) -> bool {
|
||||
REASONING_SUMMARY_UNAVAILABLE
|
||||
.get(&credentials_cache_key(credentials, model))
|
||||
.is_some_and(|learned_at| learned_at.elapsed() < REASONING_SUMMARY_UNAVAILABLE_TTL)
|
||||
}
|
||||
|
||||
/// Record that this model was refused a reasoning summary with these credentials.
|
||||
pub fn remember_reasoning_summary_unavailable(credentials: &ProviderCredentials, model: &str) {
|
||||
REASONING_SUMMARY_UNAVAILABLE.insert(credentials_cache_key(credentials, model), Instant::now());
|
||||
}
|
||||
|
||||
/// Whether a rejected request was refused over its reasoning summary, e.g. `Your
|
||||
/// organization must be verified to generate reasoning summaries` (param
|
||||
/// `reasoning.summary`). An OpenAI-kind resource can also point at a gateway that validates
|
||||
/// the body strictly and names only the unknown `summary` property.
|
||||
pub fn rejects_reasoning_summary(status: u16, body: &str) -> bool {
|
||||
// 422 is how FastAPI-based gateways reject a body that fails validation.
|
||||
if !matches!(status, 400 | 403 | 422) {
|
||||
return false;
|
||||
}
|
||||
let body = body.to_lowercase();
|
||||
let unknown_field = body.contains("additional properties are not allowed")
|
||||
|| body.contains("unrecognized request argument")
|
||||
|| body.contains("extra inputs are not permitted");
|
||||
body.contains("reasoning.summary")
|
||||
|| body.contains("verified to generate reasoning summar")
|
||||
|| body.contains("verified to stream reasoning summar")
|
||||
|| (unknown_field && body.contains("summary"))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -435,9 +509,13 @@ impl OpenAIQueryBuilder {
|
||||
tools,
|
||||
stream: Some(true),
|
||||
temperature: args.temperature,
|
||||
reasoning: args
|
||||
.reasoning_effort
|
||||
.map(|effort| ResponsesApiReasoning { effort: effort.to_string() }),
|
||||
reasoning: args.reasoning_effort.map(|effort| ResponsesApiReasoning {
|
||||
effort: effort.to_string(),
|
||||
// A request that does not reason has nothing to summarize, yet asking still
|
||||
// gets an unverified organization's request rejected.
|
||||
summary: (args.reasoning_summary && effort != REASONING_OFF_SENTINEL)
|
||||
.then(|| "auto".to_string()),
|
||||
}),
|
||||
max_output_tokens: args.max_tokens,
|
||||
text,
|
||||
prompt_cache_key: args.prompt_cache_key,
|
||||
@@ -538,6 +616,8 @@ impl QueryBuilder for OpenAIQueryBuilder {
|
||||
} else {
|
||||
Some(parser.accumulated_content)
|
||||
},
|
||||
reasoning: (!parser.accumulated_reasoning.is_empty())
|
||||
.then_some(parser.accumulated_reasoning),
|
||||
tool_calls: parser.accumulated_tool_calls.into_values().collect(),
|
||||
events_str: Some(parser.events_str),
|
||||
annotations: parser.annotations,
|
||||
@@ -637,8 +717,11 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_text_body(messages: &[OpenAIMessage], system_prompt: Option<&str>) -> String {
|
||||
let args = BuildRequestArgs {
|
||||
fn text_args<'a>(
|
||||
messages: &'a [OpenAIMessage],
|
||||
system_prompt: Option<&'a str>,
|
||||
) -> BuildRequestArgs<'a> {
|
||||
BuildRequestArgs {
|
||||
messages,
|
||||
tools: None,
|
||||
model: "gpt-5",
|
||||
@@ -652,14 +735,98 @@ mod tests {
|
||||
attachments: None,
|
||||
has_websearch: false,
|
||||
prompt_cache_key: Some(PROMPT_CACHE_KEY),
|
||||
};
|
||||
reasoning_summary: true,
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_body(args: &BuildRequestArgs<'_>) -> String {
|
||||
OpenAIQueryBuilder::new(AIProvider::OpenAI)
|
||||
.build_request(&args, &client(), "test-workspace")
|
||||
.build_request(args, &client(), "test-workspace")
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn build_text_body(messages: &[OpenAIMessage], system_prompt: Option<&str>) -> String {
|
||||
build_body(&text_args(messages, system_prompt)).await
|
||||
}
|
||||
|
||||
async fn reasoning_of(effort: Option<&str>, reasoning_summary: bool) -> serde_json::Value {
|
||||
let messages = vec![message("user", "hi")];
|
||||
let args = BuildRequestArgs {
|
||||
reasoning_effort: effort,
|
||||
reasoning_summary,
|
||||
..text_args(&messages, None)
|
||||
};
|
||||
let request: serde_json::Value = serde_json::from_str(&build_body(&args).await).unwrap();
|
||||
request["reasoning"].clone()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn requests_a_reasoning_summary_only_when_the_model_reasons() {
|
||||
assert_eq!(
|
||||
reasoning_of(Some("high"), true).await,
|
||||
serde_json::json!({ "effort": "high", "summary": "auto" })
|
||||
);
|
||||
assert_eq!(
|
||||
reasoning_of(Some("none"), true).await,
|
||||
serde_json::json!({ "effort": "none" })
|
||||
);
|
||||
assert_eq!(
|
||||
reasoning_of(Some("high"), false).await,
|
||||
serde_json::json!({ "effort": "high" })
|
||||
);
|
||||
assert!(reasoning_of(None, true).await.is_null());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognizes_a_refused_reasoning_summary() {
|
||||
let unverified = r#"{"error":{"message":"Your organization must be verified to generate reasoning summaries. Please go to: https://platform.openai.com/settings/organization/general and click on Verify Organization.","type":"invalid_request_error","param":"reasoning.summary","code":"unsupported_value"}}"#;
|
||||
assert!(rejects_reasoning_summary(400, unverified));
|
||||
assert!(!rejects_reasoning_summary(500, unverified));
|
||||
assert!(rejects_reasoning_summary(
|
||||
400,
|
||||
r#"{"detail":"Additional properties are not allowed ('summary' was unexpected)"}"#
|
||||
));
|
||||
assert!(rejects_reasoning_summary(
|
||||
422,
|
||||
r#"{"detail":[{"type":"extra_forbidden","loc":["body","reasoning","summary"],"msg":"Extra inputs are not permitted","input":"auto"}]}"#
|
||||
));
|
||||
assert!(!rejects_reasoning_summary(
|
||||
400,
|
||||
r#"{"error":{"message":"Invalid 'prompt_cache_key': string too long","param":"prompt_cache_key"}}"#
|
||||
));
|
||||
}
|
||||
|
||||
/// A resource can authenticate through `headers` with no API key: one organization's
|
||||
/// refusal must not withhold summaries from another's.
|
||||
#[test]
|
||||
fn keys_a_refused_summary_by_the_header_credential() {
|
||||
let headers = |pairs: &[(&str, &str)]| {
|
||||
pairs
|
||||
.iter()
|
||||
.map(|(name, value)| (name.to_string(), value.to_string()))
|
||||
.collect::<HashMap<_, _>>()
|
||||
};
|
||||
let url = "https://api.openai.com/v1";
|
||||
let org_a = headers(&[("Authorization", "Bearer org-a"), ("X-Trace", "1")]);
|
||||
let org_a_reordered = headers(&[("X-Trace", "1"), ("Authorization", "Bearer org-a")]);
|
||||
let org_b = headers(&[("Authorization", "Bearer org-b"), ("X-Trace", "1")]);
|
||||
|
||||
assert_ne!(
|
||||
reasoning_summary_cache_key(url, "gpt-5", None, &org_a),
|
||||
reasoning_summary_cache_key(url, "gpt-5", None, &org_b)
|
||||
);
|
||||
assert_eq!(
|
||||
reasoning_summary_cache_key(url, "gpt-5", None, &org_a),
|
||||
reasoning_summary_cache_key(url, "gpt-5", None, &org_a_reordered)
|
||||
);
|
||||
// A model can refuse summaries that another model on the same credentials streams.
|
||||
assert_ne!(
|
||||
reasoning_summary_cache_key(url, "gpt-5", None, &org_a),
|
||||
reasoning_summary_cache_key(url, "gpt-5-mini", None, &org_a)
|
||||
);
|
||||
}
|
||||
|
||||
/// The worker prepends the system prompt as a system message *and* passes it as
|
||||
/// `system_prompt`; the request must still carry it exactly once.
|
||||
#[tokio::test]
|
||||
|
||||
@@ -251,6 +251,7 @@ impl QueryBuilder for OtherQueryBuilder {
|
||||
|
||||
let OpenAISSEParser {
|
||||
accumulated_content,
|
||||
accumulated_reasoning,
|
||||
accumulated_tool_calls,
|
||||
mut events_str,
|
||||
stream_event_processor,
|
||||
@@ -277,6 +278,7 @@ impl QueryBuilder for OtherQueryBuilder {
|
||||
} else {
|
||||
Some(accumulated_content)
|
||||
},
|
||||
reasoning: (!accumulated_reasoning.is_empty()).then_some(accumulated_reasoning),
|
||||
tool_calls: accumulated_tool_calls.into_values().collect(),
|
||||
events_str: Some(events_str),
|
||||
annotations: Vec::new(),
|
||||
|
||||
@@ -27,12 +27,17 @@ pub struct BuildRequestArgs<'a> {
|
||||
/// the prefix (the step), never from the request. `None` retries a key the
|
||||
/// endpoint rejected.
|
||||
pub prompt_cache_key: Option<&'a str>,
|
||||
/// Ask for a summary of the model's reasoning where the provider streams one.
|
||||
/// `false` once the provider refused summaries to these credentials.
|
||||
pub reasoning_summary: bool,
|
||||
}
|
||||
|
||||
/// Response from AI provider
|
||||
pub enum ParsedResponse {
|
||||
Text {
|
||||
content: Option<String>,
|
||||
/// The thinking the model streamed before the answer, when it emitted any.
|
||||
reasoning: Option<String>,
|
||||
tool_calls: Vec<OpenAIToolCall>,
|
||||
events_str: Option<String>,
|
||||
annotations: Vec<UrlCitation>,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user