* fix: prevent variable push from corrupting is_secret variables
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(cli): unit-test looksLikeWorkspaceCiphertext shape detection
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): scope is_secret downgrade to single-file push, not sync push
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): warn when variable push stores a secret value as already-encrypted
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): route workspace-resolution and auth diagnostics to stderr
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(cli): rephrase comments to describe current behavior, not history
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ext-jwt): reject external JWT auth for non-existent workspaces
External JWTs are validated (not generated) on our side and never revoked
by us. The usage-tracking upsert into unique_ext_jwt_token ran
unconditionally, so a token carrying a workspace_id whose workspace no
longer exists kept refreshing its row on every presentation — surfacing
as a "new token" in the superadmin external-JWT view.
Gate jwt_ext_auth on the requested workspace existing (EE companion). When
it does not, auth fails (token is unusable) and no usage row is written.
The check is existence-only and intentionally ignores the soft-delete
flag, so deleted-then-restored workspaces keep working.
Bumps ee-repo-ref.txt to the EE companion commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(ext-jwt): cache workspace-existence lookups in jwt_ext_auth
Bumps ee-repo-ref.txt to the EE companion commit that caches the
workspace-existence check added in the previous commit, so a token aimed
at a missing workspace no longer hits the DB on every request (auth
failures aren't cached upstream).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to ac1f6f666f36141cb6ba6f8eaa614821a90464ad
This commit updates the EE repository reference after PR #626 was merged in windmill-ee-private.
Previous ee-repo-ref: e23fa03ec16909c127e8ecf0855595911c29512d
New ee-repo-ref: ac1f6f666f36141cb6ba6f8eaa614821a90464ad
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Follow-up to #9727. The orphan cleanups (cleanup_job_perms_orphaned and
cleanup_job_result_stream_orphaned_jobs) deleted at most one 100k batch per
monitor iteration. Each statement stays short and lock-light, but a single
batch per ~30s cycle caps the drain rate at ~100k/30s, so a large one-time
backlog (tens of millions of rows) takes ~hours to clear.
Loop the batched delete up to ORPHAN_CLEANUP_MAX_BATCHES (10) times per cycle,
stopping early once a batch deletes fewer than ORPHAN_CLEANUP_BATCH_SIZE rows.
Each DELETE remains bounded (≤100k, short locks, no long single statement),
while per-cycle throughput rises to ~1M rows so backlogs drain ~10x faster.
The per-cycle cap keeps monitor_db responsive.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(monitor): hash active-root exclusion in retention delete
The expired-job retention delete (delete_expired_jobs_batch) excluded jobs
belonging to still-active root flows with
`COALESCE(j.root_job, j.flow_innermost_root_job, jc.id) != ALL($3)`. That
ScalarArrayOp is evaluated per candidate row as a linear scan of $3, so cost
grows with the number of active root jobs.
Express the exclusion as `NOT IN (SELECT u FROM unnest($3) u WHERE u IS NOT
NULL)` instead. The subquery form lets Postgres build a one-time hashed SubPlan
and apply it as a filter on the ordered index scan, giving O(1) membership per
candidate while preserving the `ORDER BY completed_at ASC LIMIT` early
termination. The `u IS NOT NULL` guard sidesteps NOT IN's null-trap semantics
($3 holds non-null PK ids).
Measured on a 2M-row synthetic v2_job_completed (batch LIMIT 20000, 5-run min):
active roots | != ALL (before) | NOT IN hashed (after)
-------------|-----------------|----------------------
100 | 108 ms | 104 ms
1000 | 168 ms | 105 ms
10000 | 719 ms | 131 ms
Both forms return identical row sets (verified via EXCEPT, 0 diff). Neutral at
small active-root counts, ~5.5x faster when many flows are active.
Relates to WIN-2088
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(monitor): apply hashed active-root exclusion to log_cleanup mirror
windmill-api-settings/log_cleanup.rs::delete_expired_jobs_batch carries a
byte-identical copy of the retention delete and shared its prepared-query
cache. Updating only monitor.rs removed that shared cache entry and broke the
SQLX_OFFLINE build of the mirror. Apply the same NOT IN (hashed SubPlan)
rewrite so both copies converge on one cached query and the mirror gets the
same speedup.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The job_perms/job_result_stream_v2 orphan cleanups in monitor_db used
`NOT IN` anti-joins, `RETURNING job_id` + `fetch_all` (loading every deleted
UUID into memory) and no batch limit. On high-throughput instances these
tables can accumulate tens of millions of orphaned rows, so a single execution
ran for ~298s; because monitor_db awaits each iteration, the cleanup ran
effectively continuously, saturating DB I/O and starving audit partition
creation.
Rewrite both deletes as bounded `NOT EXISTS` anti-joins selecting `ctid` with
a LIMIT 100000, executed via `.execute()` (using rows_affected instead of
fetch_all). Each run is now fast and bounded, while the 30s monitor cadence is
preserved so the tables keep draining promptly.
Fixes WIN-2088
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The monitor loop runs ~25 periodic tasks under a single join!, so any one
stuck on a non-DB await (statement_timeout only bounds DB statements) freezes
the whole loop indefinitely — silently halting audit-partition creation. Once
the missing partition's date is reached, audit inserts fail; because login
writes its audit row in the same transaction, that poisons the login tx and
locks every user out.
- Wrap monitor_db in a 600s timeout (> statement_timeout) so a stuck task can
no longer freeze the loop; report a critical error and continue on timeout.
- After creating partitions, verify the lookahead window is actually covered
and raise a critical alert naming any missing partitions, turning a silent
latent outage into an early page.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(apps): sandbox published & raw apps with a scoped embed token
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: point ee-repo-ref at embed-token EE commit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(apps): allow top-navigation from the sandboxed app iframe
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(apps): share app localStorage across apps via the embedder
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(apps): publisher disable-sandbox option with per-version viewer consent
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(sqlx): cache for disable-sandbox queries
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: bump ee-repo-ref to disable-sandbox EE commit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): always sandbox the served raw-app wrapper + viewer fixes
The raw-app wrapper served by get_raw_app_data now always carries
`CSP: sandbox`. The publisher "disable sandbox isolation" opt-out is applied
entirely on the viewer side, which (after per-version consent) builds its own
same-origin blob wrapper — so the backend-served document stays isolated
regardless of how it is reached, never via a relaxed real-origin URL.
Also:
- CORS on the global /apps_u mount so the opaque viewer can load custom-path
public apps cross-origin.
- Reject runnable-bridge messages unconditionally until the iframe is bound.
- Relay the viewer's in-app hash up to the embedder address bar so deep links
stay shareable (hash only; embedder keeps its own pathname).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(apps): render public raw apps single-iframe (drop embed token)
Public raw apps now render directly on the real origin with a single
opaque bundle iframe and the page credential, instead of the opaque
viewer + scoped-token indirection. The author bundle stays isolated in
its own opaque iframe (CSP-sandboxed); low-code apps, whose code runs in
the viewer frame, keep the opaque viewer + scoped token.
embed_token now reports raw_app and skips minting a token for raw apps;
the access check still gates visibility.
Also set disable_sandbox: None in the remaining Policy constructors so
the full feature build (all_sqlx_features, enterprise, license) compiles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: bump ee-repo-ref to single-iframe raw-app EE commit
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(apps): grandfather existing apps as legacy-unsandboxed + authed-only consent
Existing apps are stamped by migration as `legacy_unsandboxed` so they keep
running same-origin on upgrade — no breakage and no consent prompt. New apps are
sandboxed by default; re-deploying an app clears the flag.
The publisher `disable_sandbox` consent prompt is now shown only to authenticated
viewers — an anonymous viewer has no session to expose, so the prompt was
meaningless friction.
embed_token reports `legacy_unsandboxed` and `authed`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: bump ee-repo-ref to legacy-unsandboxed EE commit
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(apps): deploy-time migration prompt for legacy-unsandboxed apps
On the first re-deploy of a grandfathered (legacy-unsandboxed) app, the
publisher must explicitly choose: enable sandbox isolation (the flag is
cleared → the app becomes sandboxed) or keep running without isolation
(→ disable_sandbox, with per-version viewer consent). updatePolicy() no
longer carries the legacy flag through a deploy, so the choice is what
sticks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(apps): disable the sandbox-isolation toggle until the app is deployed
The Deploy-drawer "Disable sandbox isolation" toggle called setPublishState()
— which updates the app by path — even before the app was first deployed, when
the path is empty, throwing an error. Guard it with disabled={!savedApp},
matching the adjacent visibility toggle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(apps): sandbox the in-workspace low-code app viewer in an opaque iframe
Extend the opaque-origin iframe isolation to the logged-in /apps/get viewer.
/apps/get becomes an embedder that keeps the workspace chrome + Edit button and
renders the app inside a cookieless, chrome-less /app_embed viewer route, handed
a scoped embed token minted from the member's session. The app frame runs in an
opaque origin (no allow-same-origin), so it cannot reach the member's session
cookie or window.parent.
- apps.rs: get_app_embed_token_for_path (authed, by-path, scope + RLS gated);
mint_app_embed_token grants a path-scoped apps:read:{path} so the viewer can
load its own app definition and no other
- lib.rs: CORS on /apps (bearer-token only, no cookies) for the opaque viewer's
by-path reads
- new /app_embed/[workspace]/[...path] viewer route (private analog of /public)
- PublicAppFrame: viewerUrl prop to point the opaque iframe at the viewer route
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(apps): unify in-workspace app viewers on the shared sandboxed path
Route every in-workspace app display (low-code and raw) through the same
PublicAppFrame -> PublicApp machinery as the public viewer, so the sandbox /
legacy-unsandboxed / disable-sandbox-consent behavior is identical on every page.
- new InWorkspaceAppViewer renders both app types via PublicAppFrame; /apps/get
and /apps_raw/get become thin wrappers over it
- /apps_raw/get previously rendered RawAppPreview directly (always isolated, with
no legacy-grandfathering or consent handling); now consistent with the rest
- retire the legacy same-origin raw viewer /apps/get_raw/[version] and re-point the
apps-list row to /apps_raw/get; remove the dead /apps_raw/[ws]/[version] route
- load the raw bundle secret in the shared viewer (getAppByPath doesn't return it)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): address PR review feedback (scope + policy hardening, nits)
- require handler-level apps:read on list_apps / list_search_apps so a scoped
embed token cannot read app definitions through the list endpoints. The route
layer treats apps:run as satisfying read; the handler check (which does not)
closes the gap.
- treat legacy_unsandboxed as backend-owned: strip any client-provided value in
create/update so it can only be set by the grandfather migration, not the API.
- document mint_app_embed_token's caller-verifies-access contract.
- use Button's declared onClick prop for the consent action (was onclick, which
fell into the rest-spread and bypassed the component's click handling).
- test: lock that the embed scopes cannot satisfy domain-level apps:read.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(apps): document embed-token endpoints in openapi + fix doc nit
Second-round review nits:
- add the three app embed-token endpoints (apps/embed_token/p/{path},
apps_u/embed_token/{secret}, and the EE apps_u/embed_token_by_custom_path) plus
the EmbedTokenResponse schema to openapi.yaml; note .html on get_data
- mint_app_embed_token doc: "Both" -> "All" (it lists three call sites)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(apps): bound embed-token scopes to the caller's own
The embed-token mint now enforces ensure_scopes_within_caller, so the
minted scope set is always within the calling credential's own scopes
(a no-op for regular unscoped sessions). Adds a unit test locking the
boundary and documents the contract on mint_app_embed_token.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): raw-app ctx in external embeds + page credential in direct render
- RawAppPreview: engage the storage relay only in opaque frames (probe Web
Storage instead of just window.parent), so a public raw app embedded in an
external iframe hydrates ctx/storage directly; add a relay-timeout fallback
so an unresponsive parent can never stall the ctx handshake.
- PublicAppFrame: in direct render, expose the page's own bearer credential
through the AuthToken context (JWT public URLs), matching the previous
route behavior; opaque-viewer mode keeps the embed token.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(apps): sandbox isolation UI polish + COI embed support for raw apps
- Deploy drawer: move the sandbox toggle out of "Public URL" into its own
"Sandbox isolation" section (the setting applies to every viewing surface,
not just the public URL), with positive phrasing, visible helper text, and
state-aware alerts (warning when disabled, info for pre-isolation apps).
Toggling it now toasts its own message instead of the login-mode one.
- Extract the deploy-time migration prompt into a shared
LegacySandboxMigrationModal built on the common Modal component, and wire
it into the raw app editor header too (it previously had no prompt, so
re-deploying a pre-isolation raw app silently changed behavior).
updateRawAppPolicy now also drops the backend-owned legacy flag, matching
the low-code updatePolicy.
- Viewer consent prompt: use the common ConfirmationModal and show the app
path (new appPath prop) instead of the route pathname, falling back to
"this app" when the path isn't known yet.
- COI embeds: propagate the wm_coep opt-in to the raw-app wrapper document
and have the backend assert COEP require-corp on it when the flag is
present — required for the bundle iframe to load when the public app page
is embedded inside a cross-origin-isolated page. Previously this only
worked in dev because the Vite proxy injects the header; the production
response lacked it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(apps): app navigation parity across sandboxed and direct viewers
- Navbar component: same-app items relay query + hash to the embedder page
(which mirrors them onto the root URL, keeping its own pathname and
transport params), app items navigate the top page through a validated
wm_embed_navigate relay instead of the cookieless viewer iframe, and
external items keep opening a new tab. Selected-item detection now
recognizes the /app_embed viewer route and ignores transport params.
- Frontend-script `goto` and button `onSuccess: gotoUrl`: same-window
navigation goes through a shared appNavigateSameWindow helper that relays
to the embedder inside the opaque viewer (same-origin paths SPA-navigate,
http(s) URLs do a full load, other schemes rejected) and keeps plain
window.location everywhere else.
- /apps/get and /apps_raw/get: key the viewer by workspace/path so in-route
navigation fully remounts it — previously the URL changed but the app (and
in sandbox mode its path-scoped token) did not follow.
- wm_embed/wm_embedder_origin added to the reserved query params so they no
longer leak into the app's ctx.query.
- Raw apps: drop the sandbox attribute entirely for the unsandboxed
(grandfathered/consented) blob path, matching the pre-isolation viewer
exactly — the attribute added no isolation there and sandboxed popups
(e.g. OAuth flows).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): preserve grandfathered policy across updates + in-workspace viewer parity
Round of compatibility hardening so pre-existing apps behave exactly as
before on every surface:
- `legacy_unsandboxed` is now preserved across app updates unless the payload
explicitly clears it (`false`, sent by the editor's migration prompt and the
sandbox toggle). Unrelated update paths — CLI / git-sync redeploys,
publish-mode toggles, cross-workspace promotion — no longer silently drop
the grandfathering. Clients still can never SET the flag.
- The embed-token endpoints (secret, path, EE custom-path) read only the
sandbox-decision policy fields, leniently, and no longer mint a token for
raw / legacy / disable_sandbox renders: the token is only consumed by the
sandboxed low-code render, and minting for the others wrote a useless token
row per view and could fail the render for scope-restricted callers.
- In-workspace viewer parity with the pre-sandbox `/apps/get`: new
`inWorkspace` mode on PublicApp (no "Powered by Windmill" badge / user
overlay, no HTML-result approval gate, column flex wrapper, `hideRefreshBar`
honored again), and the page's query/hash are forwarded into the opaque
viewer so `ctx.query` / `ctx.hash` reach the app.
- Raw apps: `window.ctx` is always `{ctx, workspace}` again (anonymous viewers
of pre-existing bundles rely on `ctx.workspace`), and the runnable bridge's
job-id scoping now applies only to sandboxed renders (`gateJobIds`) — an
unsandboxed bundle holds the same credential as the bridge, so gating there
only broke pre-existing apps polling persisted or runnable-returned job ids.
- Document `disable_sandbox` / `legacy_unsandboxed` in the openapi Policy
schema; add a unit test for the lenient policy read.
- bump ee-repo-ref to the matching EE commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): keep share-link viewer credentials out of the isolated app context
The JWT path segment of authenticated share URLs is an embedder-side
credential, consumed only to mint the scoped embed token. Two transport
channels still copied it into the isolated frame where app-authored code
runs:
- the opaque viewer iframe src defaulted to window.location.href — the
public and custom-path routes now pass a sanitized viewerUrl (JWT segment
stripped, query/hash preserved, captured once so the hash relay does not
reload the iframe);
- document.referrer on the same-origin iframe navigation carried the full
embedder URL — both app iframes now set referrerpolicy="no-referrer"
(sandboxed renders only for the raw bundle iframe, keeping exact legacy
parity; nothing reads the referrer).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(frontend): drop unused import inherited from main merge
`slide` import in AssistantMessage.svelte (from #9539) turns `npm run check`
red on this branch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): redirect the removed raw-app viewer path to the unified viewer
The old same-origin raw-app viewer route (/apps/get_raw/{version}/{path}) was
removed in favor of the sandboxed unified viewer. Re-add a thin client route at
the old path that redirects stale bookmarks to /apps_raw/get/{path}, preserving
query + hash (the pinned version is dropped — the unified viewer shows latest).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): narrow embed-token scopes and base consent on browser session
- Embed token: resource access is metadata-only (list/type/exists) via a
`resources:run` marker — resource values (get/get_value/get_value_interpolated/
list_search) are no longer reachable. Job reads are by-id only: an `app_embed`
sentinel blocks the workspace-wide job enumeration/export routes (jobs/list,
list_filtered_uuids, queue/list, completed/list, queue/export) while by-id
result polling keeps working.
- disable_sandbox consent now gates on whether the browser holds any Windmill
session (cookie-only whoami) rather than workspace-scoped auth, so a viewer
logged into a different workspace is still prompted before a same-origin render.
- db-explorer: resolve the MySQL database name server-side (the metadata query
already falls back to DATABASE()) instead of reading the resource value
client-side; getTablesByResource derives the default db from the schema.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(apps): trim embed-scope and consent comments
Reduce duplication — state the resource/job route exclusions and the
workspace-session-vs-cookie rationale once at their source and reference them
elsewhere; drop contrast/justification phrasing. No behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(apps): make app sandbox isolation opt-in (alpha)
Replace the disable_sandbox + legacy_unsandboxed policy pair and the
per-version viewer consent with a single positive `sandbox` opt-in flag.
Apps are unsandboxed by default (same-origin, full session — the
pre-isolation behavior), so existing apps are unchanged and no migration
is needed. Publishers opt an app into isolation from the deploy drawer,
flagged alpha.
- Policy.sandbox: Option<bool>; EmbedTokenResponse -> {token, expiration,
raw_app, sandbox}; mint an embed token only for sandboxed low-code apps.
- Drop the legacy-unsandboxed migration and the deploy-time migration
prompt; remove the consent modal and the browser-session probe.
- Deploy drawer: a single "Sandbox isolation" toggle (alpha), off by
default, shared by the low-code and raw editors.
- Bump ee-repo-ref to the companion EE commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): confine embed token to its intended user/folder/job routes
The embed token's broad read scopes spanned whole domains while the
matching routers are CORS-enabled for the opaque app iframe:
- users:read / folders:read were domain-wide, so the token could reach
users/list, users/list_usage, users/username_to_email/*, folders/list,
etc. Restrict to an app_embed-sentinel allowlist: only users/whoami and
folders/listnames; deny the rest of those domains.
- jobs:read allowed jobs/completed/export, missed by the job denylist.
Add it alongside jobs/queue/export.
Extend the embed-scope allow/deny test matrix to cover all of these.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(apps): align sandbox comments with the opt-in model
The consent prompt, deploy-time migration, and legacy-unsandboxed
grandfathering were removed when sandbox isolation became an opt-in
policy flag; update the comments that still described them so they
match the two-state (default-unsandboxed / opt-in-sandboxed) reality.
Comments only, no behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): confine embed-token job reads to runs the app launched
App component jobs are stamped `created_by = the viewer`, so an embed token
reads its own runs via the launched-by-viewer fast path. The token then also
inherited the viewer's broader job access (share links, folder ACLs, admin
RLS), letting user-authored app JS reuse it to read unrelated jobs by id. Stop
embed tokens at the fast path: only jobs the viewer launched, never those
merely visible to them. Return NotFound so the untrusted app can't probe
existence.
Regression test: an embed token reads its own launched job but is denied the
foreign job (result/logs/getupdate) an admin viewer's normal token can read.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): allowlist embed-token apps/jobs routes + scope run to the app
The embed token's apps:run/jobs:read reached more than a running app needs.
Replace the job denylist with strict per-domain allowlists on the app_embed
sentinel:
- Apps: only the app's own definition (apps/get/p/<path>) and the public
app-serving endpoints (apps_u/*). Denies workspace app inventory
(exists, custom_path_exists, list, list_paths*).
- Jobs: only the by-id poll routes the frontend JobLoader uses. Denies job
counts and the job_signature/resume_urls capability-minting routes (the
by-id reads remain confined to the app's own runs).
Drop unqualified apps:run from APP_EMBED_SCOPES; mint apps:run:<path> instead
and authorize apps:run:<requested path> first in execute_component, so the
token can only run its own app's components, not another app's.
Extend the embed-scope route matrix and add a path-scoped run unit test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(apps): clarify the sandbox toggle vs the on-behalf-of model
The deploy-drawer sandbox copy leaned on "session" in a way that collided
with the on-behalf-of permissioning right above it. Reword it to say the
toggle governs what the app's browser-side code can reach in the viewer's
browser — distinct from who its runnables execute as — and rename the label
to "Isolate the app from the viewer's browser session".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): path-scope embed-token S3 download to its own app
The apps_u/* allowlist also admitted apps_u/download_s3_file/<path>, whose
handler authorized any authenticated caller — so an embed token minted for app
A could download app B's S3 files via B's on-behalf policy. Add the same
path-scoped guard execute_component uses: download_s3_file_from_app now checks
apps:read:<path> first, confining the token to its own app. Other path-taking
apps_u routes are already covered (writes lack apps:write; embed_token/p
path-checks; public_resource is type-constrained).
Extend the path-scoping unit test to cover apps:read (download) alongside
apps:run (execute).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): path-scope public-app-by-secret read to the embed token's app
The apps_u/* allowlist admitted apps_u/public_app/<secret>, whose handler only
checked the viewer's read access — so an embed token minted for app A could read
app B's definition by secret (confused deputy via the viewer's identity).
get_public_app_by_secret now binds a scoped caller to the resolved app with
check_scopes(apps:read:<path>), confining it to its own app; unscoped sessions
and anonymous access are unchanged.
get_raw_app_data needs no binding (pure secret capability, no caller identity).
Document the full set of app-resolving handlers the path-scoped read covers.
Bump ee-repo-ref for the companion custom-path fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): preserve pre-sandbox behavior for db-explorer, edit link, jwt
Three behavior-parity fixes for non-sandboxed (existing) apps that the
sandbox-isolation refactor changed incidentally:
- DB-explorer MySQL table picker: when the connection can see multiple
non-system schemas, label the default db's tables unprefixed again. The
resource-value read was removed globally, so identify the default db from
the introspection script's `DATABASE() AS default_db_name` (carried on
SQLSchema.defaultDb) instead of guessing "the single schema key". Equivalent
to the prior resource.database match; editor-only (table picker).
- In-workspace Edit button: restore `?nodraft=true` on both /apps/get and
/apps_raw/get, so opening the editor from the viewer loads the deployed
version, not a draft.
- Custom-path (/a) viewer: restore the "could not authenticate user with jwt
token" toast when a path JWT fails to resolve a user, instead of silently
falling through.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): confine embed-token S3 downloads to the app's own keys/outputs
download_s3_file_from_app authorized any authenticated caller for any S3 key
(opt_authed.is_some() bypass). A sandboxed app's embed token carries the
viewer's identity, so app-authored JS could fetch arbitrary S3 keys readable by
the on-behalf identity, beyond the app's own declared keys or outputs.
Route app embed tokens through the same allowlist as anonymous viewers — the
app's declared allowed_s3_keys, or files produced by this app's own component
runs — instead of the authed bypass. The produced-files check is parameterized
by created_by (the embed viewer for a token, else anonymous) so a sandboxed
app's own S3 outputs still render while arbitrary keys are denied.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): let embed tokens cancel their own jobs; gate cancel to launcher
A sandboxed low-code app supersedes an in-flight component run on re-run by
canceling it, but the embed token only had jobs:read, so cancellation silently
failed and prior jobs ran to completion.
- Permit the by-id jobs_u/queue/cancel POST for app_embed tokens at the route
layer (the only write reachable through the existing by-id allowlist).
- Gate cancel_job_api: an app_embed token may cancel ONLY jobs it launched
(created_by == viewer). cancel_job_api had no other per-job ownership check,
so this also confines the token instead of letting it cancel any job by id.
- /app_embed now sets workspaceStore so cancellation targets the right
workspace instead of an empty/stale one in the cookieless iframe.
Add a shared has_app_embed_sentinel helper; cover cancel in the route matrix
and the jobs_read_auth integration test (own job cancelable, foreign denied).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): drop get_root_job_id from the embed-token job allowlist
Audit of the embed token's reachable job routes: get_root_job (jobs_u/
get_root_job_id) has no access check in its handler at all — it returns any
job's root-job id by id — and the app runtime never calls it. Remove it from
the by-id allowlist so the embed token can't probe a foreign job's flow lineage;
add a denied-route assertion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(apps): scope sandboxed-app localStorage per app
Sandboxed apps shared one localStorage store (one key on the real origin), so an
app could read or clobber another app's keys — and, with job ids stashed there,
reuse its embed token to read another app's job. Scope the backing store per app.
The embed-token endpoints now return the resolved app_path (EmbedTokenResponse;
not a new disclosure — the viewer already receives the path when it loads the
app). PublicAppFrame (low-code) and RawAppPreview (raw) key their backing store
by it: wm_apps_localstorage:<app_path>. Same app shares one store across its
public and in-workspace surfaces; different apps are isolated. Unsandboxed apps
are unaffected (real same-origin localStorage, as before).
Bump ee-repo-ref for the companion custom-path change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): scope embed access checks to embed tokens + key app storage by workspace
- Apply the path-scoped read/run checks on the public-by-secret read and the
component run path only when the caller is an app embed token, so other
caller types keep their prior access.
- Key the sandboxed app's backing client storage by workspace + path instead
of path alone, and return the resolved workspace from the embed-token
endpoints so the custom-path viewer can derive it.
- Show a clear message instead of an indefinite loader when the viewer route
is opened outside its embedder.
Bumps ee-repo-ref to 5b8476b.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): mint embed tokens only from the trusted embedder caller
An app embed token must not reach the embed-token mint endpoints; refresh
minting stays with the embedder session/JWT. Enforced at the scope route
layer and at the mint chokepoint, with a route-matrix regression test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(apps): support S3 upload and frontend-script S3 download in sandboxed apps
Sandboxed apps run with a scoped embed token (no cookie). Let the app's
S3 file-input upload and the frontend-script download({s3}) helper work in
that context: upload is reachable with apps:run and re-checked per-app at the
handler; the script download routes through the app-scoped apps_u endpoint
with the embed token instead of the cookie-authed job_helpers path. Default
(unsandboxed) apps are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: update ee-repo-ref to b0cb761bf9852974e571b2978032d310cc998517
This commit updates the EE repository reference after PR #600 was merged in windmill-ee-private.
Previous ee-repo-ref: e673c714a4618fdb72353a475f49c748e6016642
New ee-repo-ref: b0cb761bf9852974e571b2978032d310cc998517
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
The /api/health/status database check used `SELECT 1`, which succeeds
even on a read-only standby. After a PostgreSQL failover where the
primary becomes a secondary, the health check kept reporting healthy
while all writes failed with "cannot execute INSERT in a read-only
transaction", so Kubernetes liveness probes never restarted the pod.
Use `SELECT NOT pg_is_in_recovery()` instead: it returns true on a
primary and false on a standby, so a read-only replica is now reported
unhealthy. Result handling checks the returned bool (Ok(Some(true)))
rather than just query success.
Fixes WIN-2085
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): workspace ai_skill table + CRUD API
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): AI Skills workspace settings tab with SKILL.md upload
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): advertise skills in global system prompt + read_skill tool
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(ai-chat): move custom skills into AI settings (paste or folder)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): cap folder import (depth<=3, max 50 skills, confirm dialog)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(ai-chat): give import folder its own labeled subsection
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): resolve svelte-check never-narrowing in skills preview
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: address ai skills review issues
* fix: validate ai skills and reload workspace list
* fix(ai-chat): spec-align skill validation and cap skills per workspace
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): reject duplicate skill uploads, audit skill names
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): sync deref openapi specs with skill validation rules
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Flows with no defined inputs can produce a sparse schema (e.g. { order: [] })
that lacks the "type": "object" field. buildSchemaForTool spread this schema
into the tool parameters as-is, so the Anthropic API rejected the tool
definition with `400 invalid_request_error:
tools.N.custom.input_schema.type: Field required`. The existing fallback in
anthropic.ts only triggers when parameters is falsy, but the sparse schema is
truthy.
Default type:object before spreading the schema in buildSchemaForTool, and
backfill type/properties/required in FlowAIChat's getFlowInputsSchema as a
defense-in-depth measure.
Fixes WIN-2087
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: re-enforce per-path token scope on store rename, delete and interpolation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: enforce token scope on workspace export and resume-url minting
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: enforce per-item and runnable scope on trigger create paths
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: enforce app write scope before persistence and on rename
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: enforce scope containment on mcp oauth approval
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: scope mcp endpoint-proxy jwt to the proxied route
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: treat resource-linked variables and resources as covered by resource scope
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: only require variables:read for plaintext-secret workspace export
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: handle singlestepflow resume, reject empty mcp grant, scope var-skipped tarball
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Switching workspaces created a new WebsocketProvider without destroying
the old one. The leaked provider kept reconnecting, causing alternating
websocket traffic between old and new workspace rooms and flickering in
the Live Activity sidebar.
Add a disconnectWorkspace() cleanup that destroys the provider and resets
connected/awareness state, call it at the start of connectWorkspace()
before creating a new provider (matching ScriptEditor.svelte), and run it
from an onDestroy hook so the provider is torn down on unmount.
Fixes WIN-2086
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): strip raw-app post-deploy diff noise (raw_app/lock/data)
The raw-app editor's Diff drawer showed a spurious deployed-vs-current
diff immediately after deploy, even with no edits: `raw_app: true`, a
server-recomputed inline-script `lock`, and an empty `data` mismatch.
These come from comparing the deployed app row (from getAppByPath) against
the editor's current value, which differ on server-managed fields the
editor never carries, on inline-script locks (recomputed at deploy, cleared
on edit), and on `data` (the deployed row omits an empty `data` while the
editor always carries the default `{tables: []}`).
Add `stripRawAppDiffNoise` (strip server columns, null inline locks,
canonicalize data) and apply it symmetrically to both diff sides in the
editor header. For the session/compare draft diff, the draft is stored flat
(files/runnables/data top-level) while the deployed row nests under `value`,
so add `canonicalRawAppDiffValue` (= appSourceToDraftValue + stripRawAppDiffNoise)
and route both sides through it in getDraftDiffValues. Both diff surfaces now
share the same normalizer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): use canonicalized current value in deploy-drawer raw-app diff
The Deploy drawer's "Diff" action still built the current side inline from
raw editor state, bypassing stripRawAppDiffNoise — so inline-script `lock`
and data-shape noise could resurface via Deploy → Diff even though the
top-level Diff button was already fixed. Route it through `currentDiffValue`
(and strip the savedApp fallback) so both entry points behave identically.
Addresses Codex review finding on PR #9706.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: ignore NotFound errors when deleting log files from object store
Periodic and manual log cleanup delete log files from instance object
storage. S3's DeleteObjects silently ignores missing keys, but GCS
returns a 404 for each individual delete, which the object_store crate's
default delete_stream surfaces as Error::NotFound. This produced noisy
error/warning logs on every cleanup cycle even though the cleanup
succeeded (DB records are removed regardless).
Treat a NotFound delete as a successful no-op in both delete handlers:
- monitor.rs: skip logging NotFound errors
- log_cleanup.rs: count NotFound as deleted instead of an error
Fixes WIN-2081
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: report 404 (already-absent) count in object store log cleanup
Track delete calls that returned 404 (object already absent) separately
from real deletes so operators can see how many of the attempted deletes
were no-ops, instead of those numbers silently folding into s3_deleted.
- monitor.rs: emit a final info summary per cleanup cycle:
"N deleted, M already absent (404), K failed" (only when work occurred)
- log_cleanup.rs: add s3_not_found to LogCleanupProgress (serde default for
backward-compatible deserialization of in-flight rows), thread it through
s3_bulk_delete and all call sites, and log a final summary on release
- openapi.yaml + generated client + ObjectStoreConfigSettings.svelte:
surface the 404 count in the manual cleanup status UI
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: import ObjectStoreError directly from object_store_reexports
The object_store_reexports module already re-exports object_store::Error
under the name ObjectStoreError, so `Error as ObjectStoreError` failed to
resolve (no `Error` in that module). This compiles only behind the
parquet feature, which the local dev `cargo watch` doesn't enable, so it
was caught by CI's full-feature check rather than locally.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(oauth): restore bring-your-own CC token URL override
Re-add the optional resource-level token URL field for client-credentials
connections, sent only with the caller's own client_id/secret. Updates the
connect/create_account request schemas and bumps the EE ref.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(oauth): keep openapi-deref unchanged from main
The dereferenced specs are not regenerated per-PR (already stale on main,
CI only lint-validates them). Revert the incidental full regen so the PR
diff stays focused on openapi.yaml.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(oauth): host-pin CC token URL override server-side
Add is_instance_templated_cc so the EE handlers can reject a bring-your-own
token URL override for {instance}-templated providers (defense in depth for
direct API callers). Bump the EE ref.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(oauth): serve cc_token_url in deref specs, enforce CC grant gate
Add cc_token_url to the dereferenced OpenAPI artifacts served at /openapi.yaml
and /openapi.json so generated clients see the new field (kept to a focused add
rather than a full regen). Bump the EE ref for the grant-gate enforcement.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to de49fda2320504ad9e7d2d31c7033d71dbf6ca43
This commit updates the EE repository reference after PR #625 was merged in windmill-ee-private.
Previous ee-repo-ref: a939228d0314c21937687d43c8ef354bdc87c40e
New ee-repo-ref: de49fda2320504ad9e7d2d31c7033d71dbf6ca43
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* ci: add path-gated AI agent integration tests workflow
Runs integration_tests/ai_agent_tests against real LLM providers
(Anthropic/OpenAI/Google) only when AI-agent backend code or the tests
change, since runs make paid LLM calls. Adds a conftest fixture that
skips provider-parametrized cases whose API keys are absent, so CI
exercises only the providers it has secrets for.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: add path-gated ai_evals global-mode smoke workflow
Runs the global AI chat eval (global-test1) across one cheap model per
provider (Anthropic/OpenAI/Google/DeepSeek) only when the eval harness or
copilot chat code change, since runs make paid LLM calls. Builds Windmill
CE from source as the AI proxy; global tools/drafts run in the Vitest
bridge. Gates on the deterministic draft pipeline (run succeeded +
produced a draft + used write_script), not the variable LLM judge score.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: run AI smokes on PR ready-for-review instead of every push
Switch the pull_request trigger from `synchronize` (every commit) to
`ready_for_review`, with a job guard skipping draft PRs, so the paid LLM
runs only fire when a PR is marked ready to merge (plus push-to-main and
manual dispatch).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai_evals): lazily load cli mode so non-cli evals skip the cli toolchain
The entrypoint eagerly imported modes/cli, which pulls the wmill CLI
guidance modules and their JSR deps (@cliffy/*). Global/flow/script/app
runs then crashed with "Cannot find module '@cliffy/ansi/colors'" when
the cli workspace deps were not installed. Import createCliModeRunner
dynamically inside runCliBenchmark instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ai_agent): raise low max_completion_tokens to OpenAI's 16 minimum
OpenAI's /v1/responses rejects max_output_tokens < 16 with a 400, failing
test_low_max_tokens for openai. 16 still exercises a truncated response.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: run ai_evals workflow on Node 22 for the frontend undici 8.x dep
The Vitest bridge loads frontend/node_modules/undici@8.x, which requires
Node >=22.19; Node 20 failed with "webidl.util.markAsUncloneable is not a
function" when loading vitest.config.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai_evals): run frontend evals autonomously + give global-test1 more turns
Frontend evals (flow/script/app/global) ran the production chat prompt, which
assumes an interactive human — so cheaper models burned their turn budget
asking for confirmation, waiting for approval, or presenting a plan, sometimes
hitting maxTurns without producing a draft. Append a shared autonomy note in
baseEvalRunner (the path all frontend modes share, mirroring cli mode): act
directly on clear requests; only ask on genuinely ambiguous ones (preserving
the askUserQuestion cases). Also raise global-test1's maxTurns 8 -> 10 so a
model that over-explores still converges.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci(ai_evals): watch draft/prompt deps outside copilot/
The global eval runs production frontend code in-process, so the smoke's
behavior depends on files outside frontend/src/lib/components/copilot/**:
the draft model (userDraft.svelte.ts, userDraftDbSyncer.svelte.ts), script
inference (infer.ts), and the chat system prompts ($system_prompts ->
system_prompts/auto-generated). Add them to both push and PR path filters so
a change there actually triggers the smoke that gates on draft production.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: skip direct provider tests without credentials
* feat: add ai evals skip judge flag
* fix: simplify ai evals ci gate
* fix: simplify ai evals smoke gate
* fix: handle ai eval workflow triggers
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: add global-mode path-selection eval cases with seeded user
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(copilot): guide global-mode path selection with injected folder list
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(copilot): tailor global-mode folder guidance for workspace admins
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(copilot): type folders_read; isolate global-eval user from store
Addresses PR review:
- Add folders_read to the User/whoami openapi schema and UserExt; the global prompt builder and eval harness now read it typed instead of via inline casts (regen the client to pick it up).
- prepareGlobalSystemMessage takes an explicit user; the eval harness passes it rather than mutating the process-global userStore, removing the concurrency race (path cases no longer need --verbose).
- Rewrite the path-selection case comment as a current invariant.
- Add buildFolderGuidance unit tests in core.test.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: round worker usage occupancy up to 5-minute chunks
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 8b12fa14ef7969e948169eadf1bf672d7928e5b1
This commit updates the EE repository reference after PR #624 was merged in windmill-ee-private.
Previous ee-repo-ref: 168498974150ff309658b2da43bce8444f13a765
New ee-repo-ref: 8b12fa14ef7969e948169eadf1bf672d7928e5b1
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix(frontend): deploy full script/flow draft from AI chat via shared module
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): drop non-persisted priority/timeout from flow draft deploy
The flow branch of the shared deployDraft set `priority`/`timeout` on the
create/update body, but the backend does not persist those fields on flows
(a direct API write returns them as null). Remove the dead fields and the
unit-test assertions for them; the flow deploy still carries every config
field the backend actually stores (tag, dedicated_worker, …).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): chat deploy resolves draft storage path (honor chosen path)
The chat addresses drafts by their display/chosen path, but a draft_only item
created in the editor lives at a synthetic `u/{user}/draft_{uuid}` storage key
(chosen path held in the draft value). The shared deployer reads the draft via
getScriptByPath/getFlowByPath at the path passed, so passing the chosen path
404'd. Resolve to the storage path via getGlobalDraftStoragePath before
delegating; the deployer then deploys at the draft's own `path`. Regression from
the deploy-unification: the old builder read the already-resolved draft and
deployed at the chosen path directly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): chat raw-app deploy honors the draft's chosen path
The raw-app branch deployed at the path the chat was addressed by (args.path),
which for an editor-created draft_only raw app is the synthetic
`u/{user}/draft_{uuid}` storage key, not the chosen path. Resolve the storage
path and read the chosen path from the backend raw_app draft's `draft_path`
(confirmed shape: getAppByPath{getDraft,rawApp}.draft.draft_path), then create/
update there — mirroring the script/flow storage-path resolution. Content still
comes from the flat AppDraftValue, which the editor and chat both use.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): flush live draft before chat deploy; narrow raw-app catch
Addresses review feedback on the AI-chat deploy:
- Codex P1: script/flow deploy delegates to the shared deployer, which re-reads
the persisted DB draft. An open editor's edit may still be parked in a
debounced/disabled autosave, so the deploy could publish a stale draft and the
post-deploy draft delete could drop the unsaved edit. Flush the draft's
UserDraftDbSyncer key before delegating (always saves, like Ctrl/Cmd+S, since
the user explicitly asked to deploy).
- Cubic P2: the raw-app draft_path lookup caught all errors and fell back to the
storage path, masking real failures (network/5xx). Only fall back on 404;
re-throw other errors so the deploy aborts instead of deploying to the wrong path.
Adds tests for both; updates the existing raw-app deploy tests to mock getAppByPath.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): flush raw-app draft before reading draft_path on chat deploy
Codex P1 follow-up: the raw-app branch derives the deploy targetPath by re-reading
draft_path from the persisted backend draft, but — unlike script/flow — didn't
flush first. An editor rename mirrored into draft_path can still be parked in a
debounced/disabled autosave, so an immediate chat deploy could read a stale
draft_path and deploy to the old path. Flush the raw_app draft key before the
getAppByPath read, mirroring the script/flow fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): abort chat deploy when pre-deploy draft flush conflicts/fails
Codex P1 follow-up: the pre-delegation UserDraftDbSyncer.flush() resolves even
when the save recorded a conflict (server has a newer version) or failed
(network/5xx) — it does not throw. The deploy would then re-read a stale or
conflicting persisted draft and publish it. Add flushDraftOrThrow(): after flush,
check getConflict() and getState().state === 'failed' and abort with a clear
message. Used by both the script/flow and raw-app deploy paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: default instance db name to dt_/dl_ workspace scope
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: cap instance db name at 63 chars and add unit tests
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: add file attachments to the global AI chat
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: add folder linking and file-type icons to chat attachments
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: persist linked files, add @-menu file tree, and polish chat file UI
Persistence (survive reload, scoped to session.id):
- IndexedDB store (attachedFilesDB) holding Blob snapshots (every browser)
and re-grantable File System Access directory handles (capable browsers)
- restore on session activation; re-grant locked handles on the next send;
flush in-memory items when the session persists; GC on session delete
- capability via feature-detection (fsAccess), never UA sniffing
- folders auto-refresh (live re-enumerate + reconcile) on each send
@-mention file picker:
- Files branch in ChatContextPicker (new DrillPicker architecture); a linked
folder's files render as a nested directory tree, picking inserts @filename
- attached-file mentions highlight in the input just like context mentions
UI polish:
- file/folder chips reuse the context-element chip style (icon -> X on hover)
- file + context badges sit above the fork/draft bar
- disabled dropdown items can surface an explanatory tooltip (DropdownV2Inner)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor: deepen the attached-files store — folders as first-class objects
Two seam fixes from an architecture pass, no behaviour change:
- addFolder(dirHandle) now enumerates internally (same junk-filtered walk
used on restore/refresh), so callers never pre-enumerate. The dead
drop-walkers (collectDroppedEntries, filterFolderPickerFiles) are deleted;
isIgnoredPath/MAX_FOLDER_FILES move next to enumerateDir in fsAccess.
- The store exposes `folders` (name + aggregate status + children) and
`standalone` as derived views, so the bar, the @-menu picker, the folder
chip and the system-prompt roster stop re-grouping the flat row list and
re-deriving folder status. Placeholder rows (isFolderRoot) become an
implementation detail; the roster renders a locked folder as one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: drop the redundant context-badge row in the global chat
In GLOBAL mode selected context already appears as a highlighted @mention
in the input (deleting the mention deselects), so the hoisted badge row
above the chat duplicated it. File chips keep their row — attachments
aren't represented in the input.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: harden attachment edge cases found in review
- requestReadPermission/queryReadPermission never reject (the spec rejects
with SecurityError when user activation is missing — now mapped to
denied/prompt), and sendRequest wraps attachment upkeep in try/catch, so
a permission hiccup can never silently swallow a Send.
- regrantLocked expands before dropping the locked placeholder: when the
re-granted directory is gone from disk, the folder now shows
"unavailable" instead of vanishing into a zombie that resurrects locked
on the next reload.
- addFolder: re-picking a locked/unavailable folder relinks it (natural
recovery gesture); a genuine second folder with the same basename gets a
visible "already linked" rejection instead of a silent no-op.
- fileEngine: readFile clamps its byte slice to maxChars*4 before decoding
and streamLines caps its per-line buffer, so newline-sparse files
(minified JS, single-line JSONL) can't materialize unbounded strings;
corrected the scan-cap comment's claim about catastrophic backtracking.
4 new unit tests (41 total).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: surface folder-picker failures instead of swallowing them
`pickDirectory` caught every `showDirectoryPicker` rejection and returned
undefined, so a real failure (an enterprise/browser policy blocking the File
System Access API, a lost user-activation, …) was indistinguishable from a
no-op — the picker just silently never opened. Now only `AbortError` (user
dismissed the dialog, or CDP intercepted it under automation) is treated as a
cancel; anything else is rethrown and `linkFolder` surfaces it as a toast.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: support folders in browsers without the File System Access API
Folders can now be added in every browser, not just Chromium. Where the File
System Access API is absent (Firefox/Safari), a dropped or picked folder's files
are snapshotted into the browser (via a webkitGetAsEntry drop-walk or a
`webkitdirectory` input) instead of linked as a live handle, and grouped/displayed
identically to a File System Access folder. The dropdown item reads "Link folder"
when a live link is possible and "Add folder" otherwise, with a tooltip pointing
to Chrome/Edge for a live link.
Snapshot folder children persist their `folder`/`relPath`, so they regroup into
the same folder chip on reload.
Removes the arbitrary file-count caps (500 per folder, 100 total) — only the
browser's memory / IndexedDB quota now bound a folder. Junk paths
(node_modules/.git/dist/dotfiles) are still skipped, folder-contents only, so an
explicitly attached standalone dotfile is kept.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: address review feedback — index-race guard + read_file line numbers
Both automated reviewers flagged two issues on the attached-files feature:
- (P1) Stale async indexing could corrupt a newer file. `#indexFile` applied
its unawaited `buildLineIndex` result by display name, so if a row's file was
swapped while indexing was in flight (remove + re-add a same-named file, or a
folder refresh re-indexing an edited file) the stale result stamped the wrong
lineIndex/lineCount — and `read_file` then sliced the new Blob with old
offsets. Now patched via `#patchFile`, which applies the result only while the
row still holds the exact file object that was indexed.
- (P2) `read_file` promised "line-numbered context" but returned raw text. It now
prefixes each line with its absolute 1-based number (`<n>→<content>`), matching
the tool contract; `numberLines` lives in fileEngine and is unit-tested.
Adds regression tests: a deterministic stale-index race test (controlled
buildLineIndex ordering) and numberLines coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: address re-review nits — read_file pagination + searchFiles regex state
- read_file: when the maxChars cap truncated a window short of its requested
end line, the pagination note still reported the full range and gave no/wrong
resume point, so the model couldn't reach the unread lines. The note now
reports the last line actually returned and resumes at the next unread line
(advancing past a single over-long line rather than re-truncating it forever).
- searchFiles: reset `regex.lastIndex` before each `.test()` — a caller-supplied
`g`/`y` flag makes test() stateful and would silently drop matches. Not
reachable from the current caller, but searchFiles is exported.
Adds regression tests for both.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: keep an emptied live folder linked and refreshing
A live (File System Access) folder carried its directory handle only on its child
file rows. When the folder was emptied on disk, refreshFolders/#reconcileFolder
removed the last child — dropping the only handle-bearing row — so the folder
vanished from the chip bar AND was never re-enumerated again (files added back on
disk weren't picked up until a reload). #expandFolder had the same gap on restore.
Now #ensureFolderRow leaves one handle-carrying placeholder row when a folder has
no readable children (keeps the chip visible and the live source alive), and drops
it once children return; refreshFolders collects sources from placeholder rows too,
and readyFiles never exposes a placeholder to the read/search tools. Adds a
regression test (empty → still visible → file returns → picked up).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: trim read_file char-cap output to match its pagination note
When the char cap cut partway into the line after some whole lines, readFile set
the note/endLine to the last complete line but still returned the partial next
line in `text` — so read_file showed (line-numbered) a line the note said would
come on the next read. Trim the returned text back to the last complete newline
so the body and the note agree. Test now asserts res.text for that case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: isolate search_files in a Worker (ReDoS) + path-aware folder dedup
- search_files runs a model-supplied regex, and a catastrophic-backtracking
pattern (e.g. /^(a+)+$/) can't be interrupted mid-test, freezing the tab. Run
the search in a Web Worker (searchFilesInWorker) and terminate it on a timeout,
returning "pattern too expensive" instead of hanging. Degrades gracefully to a
main-thread search where Workers are unavailable / fail to load.
- #isDuplicate keyed its content check on the file basename, so two distinct
files sharing a basename under different folder subdirs (proj/a/index.ts vs
proj/b/index.ts) were wrongly deduped and silently dropped from snapshotted
folders. Key it on the relative path instead.
Adds tests: worker result/timeout-and-terminate, and same-basename-different-subdir.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: keep an initially-empty live folder linked (placeholder + persist)
addFolder only created rows / persisted the dir-handle when at least one text file
was found, so linking a folder that's empty (or all-binary) at pick time was a
silent no-op: no chip, nothing persisted, and refreshFolders had no source to
re-enumerate when files were added later. Now it always leaves a placeholder
(#ensureFolderRow) and persists the handle — matching the became-empty behavior —
so the folder stays visible, survives reload, and picks up files added afterward.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: keep empty-folder placeholders out of the real-file name space
The placeholder row for an empty live folder uses name = folder, which could
collide with a standalone file of the same name: addFiles deduped the file
against the placeholder, removeFile(name) dropped both rows, and #uniqueName
pushed the file to a "(2)" suffix. Placeholders are managed via removeFolder and
never read by the tools, so exclude isFolderRoot rows from #isDuplicate,
removeFile, get(), and #uniqueName. Adds a placeholder/standalone collision test.
(codex's other nit — @-mentions not highlighting filenames with spaces — left as
a known cosmetic limitation per the chosen scope.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: highlight @-mentions of filenames containing spaces
A file mention was inserted verbatim as `@my file.txt`, but the highlighter regex
`@[\w/.\-\[\]]+` stops at the space, so only `@my` was parsed/highlighted and the
mention didn't behave as advertised. Introduce a small shared `mention` module:
names with whitespace are inserted in a bracketed form `@[my file.txt]`, and the
shared regex + `mentionTitle` parse both bare and bracketed tokens. Both insertion
entry points (the inline `@` picker in ContextTextarea and the toolbar path in
AIChatInput) now use `formatMention`, so the full name highlights.
Verified in a real browser: `@[my file.txt]` renders as a single highlight span.
Unit tests cover format/parse/round-trip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: search_files reports a requested file's real status, not "not attached"
search_files filtered the store down to readyFiles() before validating a
requested `file`, so searching an attached-but-not-ready file (indexing / errored
/ locked / unavailable) while another file was ready returned "No attached file
named X" — even though it is attached. Factor read_file's status reporting into a
shared notReadyMessage() and have search_files report the same accurate status
before searching the ready subset.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: clear attached files on new/loaded chat in the non-session global chat
saveAndClear() (the "New chat" button) and loadPastChat() left attachedFiles
intact. In an AI session that's intended — files are session-scoped and persist
across conversations. But the ephemeral global side-panel chat has no session, so
the next, unrelated conversation still got the previous file roster injected and
could read_file/search_files against it. Clear attachments on both transitions
when `!isSessionChat`; sessions keep them. Adds a lifecycle regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: keep an empty folder linked when regranting access after reload
regrantLocked() dropped the locked placeholder unconditionally after #expandFolder.
If the regranted folder was empty (or all-binary), #expandFolder's #ensureFolderRow
no-op'd (the locked placeholder still existed), so dropping it removed the only
handle-bearing row — unlinking the folder and stopping future refreshFolders from
ever seeing files added back. Re-ensure a ready placeholder after dropping the
locked one. Adds a regression test for the empty-regrant path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: round-trip @-mentions of filenames containing a closing bracket
The bracketed mention form `@[name]` broke when the name contained a `]`
(e.g. `notes ] draft.md`): the regex stopped at the first `]` and mentionTitle
resolved the wrong name, so it wouldn't highlight. Escape `\` and `]` when
bracketing, match escaped chars in MENTION_RE, and unescape in mentionTitle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: highlight @-mentions of filenames with HTML-sensitive / special chars
getHighlightedText() escapes the textarea value to HTML before parsing mentions,
then looked the parsed title up against raw attached names — so a file like
`R&D notes.md` (escaped to `R&D notes.md`) never matched and wasn't highlighted.
Also, names with chars outside the bare set (`<`, `>`, `&`, parens, …) weren't
bracketed, so the bare regex truncated them. Now formatMention brackets any
non-bare-safe name, and the highlighter HTML-unescapes the parsed title before the
store lookup. Verified in a real browser with `R&D notes.md`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: report the real reason search_files has no readable targets
When attachments existed but readyFiles() was empty, search_files always told the
model "still being indexed, try again shortly". That's wrong for the placeholder
states this PR introduces: an empty or binary-only linked folder leaves only a
filtered-out `ready` placeholder, and a locked/unavailable restored folder exposes
no readable children. Now the message reflects the actual state — no searchable
text, restore access, or re-link — and only says "indexing" when something is.
Adds a focused fileTools test for the empty-ready states.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(frontend): hide primary storage row until added in workspace storage settings
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): red border on empty storage resource picker
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): allow deleting primary storage when no secondary storages exist
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): disable storage save when a row is missing its resource
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(jobs): auto-grant approvers a run-detail view link on the approval page
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(approval): show a clear run-details button for logged-in workspace members
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(approval): build run-details link from route params, not undefined job
getJob is fire-and-forget on the new approval page and is denied for an approver who lacks direct run read access — the exact case this link serves — leaving job undefined and producing /run/undefined. page.params.job is the flow id the view_token is minted for.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The debounce assertion resolved the dispatched job's debounce window through
`prefetch_cached_from_handle`, which goes through the process-global
runnable-settings cache (shared by every test running concurrently in the
binary) and its tempdir-backed file I/O. Read the persisted rows directly from
the test's isolated DB instead, removing that cross-test coupling and extra I/O
from the assertion path. Still validates the full wiring
(handle -> runnable_settings -> debouncing_settings).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(home): preserve URL filter state on page refresh
ListFilters.loadFilterFromUrl() runs synchronously at script init and
sets ownerFilter via binding. When $workspaceStore resolves asynchronously
after mount it triggered the $effect that resets ownerFilter, wiping the
URL-loaded filter before the user saw any results.
Skip the first $workspaceStore resolution using the same firstRun guard
pattern already used in this file (firstWorkspaceRun). Workspace switches
still correctly clear the filter.
Fixes#9624
* refactor(home): reset filters on workspace change instead of first-run guard
Track the previous workspace value and clear filters only when it actually
changes, rather than skipping the first $workspaceStore resolution. Encodes
the real invariant (reset on change) without depending on child/parent init
ordering, and preserves URL-loaded filters on initial mount by construction.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: validate websocket trigger urls and gate trigger test route
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: clarify validate_websocket_url_for_ssrf call sites
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): refetch pipeline dispatch edges on live runs so cascades group
The activity panel groups runs into cascades by connected components of the
dispatch-edge graph, but edges came only from the one-shot history preload
(refetched on workspace/folder/days change). dispatch_event rows are written
server-side when a producer completes, so a run launched live had its producer
and freshly-dispatched children appear as live poll events with no connecting
edge — they rendered as separate ungrouped rows instead of one cascade.
Add an edges-only refetch and trigger it whenever the live poll's event id-set
changes, so live cascades converge to grouped like historic/scheduled ones.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): address review nits on live dispatch-edge refetch
- Sequence same-scope edges-only refetches with a monotonic edgeSeq so a
slower earlier response can't overwrite a newer one mid-cascade (the gen
counter only guards scope changes).
- Condense the duplicated edge-refetch rationale: keep the canonical "why"
in loadEdges, trim the page effect comment to its trigger/loop invariant.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(backend): only emit asset producer-change event on write-set changes
Data Pipelines (#9193) made every script deploy emit a
`notify_asset_producer_change` event: `clear_static_asset_usage` inserted
into `notify_event` unconditionally on every clear, and the per-asset
insert path emitted nothing. So a plain script with no assets — the
overwhelming majority of deploys — wrote a `notify_event` row that made
every worker drop its `ASSET_PRODUCER_WRITES_CACHE` entry instance-wide,
needlessly thrashing the cache the feature added.
That cache only tracks script rows with write access (`usage_access_type
IN ('w','rw')`), so a deploy changes it only when the script gains or
loses a write producer. Gate the event on exactly that:
- `clear_static_asset_usage` / `clear_static_asset_usage_by_script_hash`
emit only when the delete removed a 'w'/'rw' row (via `RETURNING`).
- `insert_static_asset_usage` emits only when it actually inserts a
'w'/'rw' script row (no-op `ON CONFLICT`, read-only, and flow usage
stay silent).
Plain, read-only, and flow deploys now emit nothing; producer-changing
deploys still invalidate, atomically and visible-only-on-commit as
before. Adds a test asserting the emit/no-emit matrix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(backend): dedup producer-change notify on write-asset redeploys
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(backend): derive replace write-set from persisted rows; document auth contract
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(backend): correct replace_static_asset_usage call-site comment
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add global AI chat context-optimization plan for raw apps
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ai-evals): add global raw-app debugging cases on a large fixture
Adds a ~20-file analytics_dashboard raw-app fixture (incl. a 5k-line data module
and a planted wrong-totals bug), two global cases (read-heavy debug + small-edit
baseline), app-seed support in the mock backend, directory-fixture loading, and a
decorateHelpers seam so read-dedupe is measurable. Records tokenUsage for before/
after comparison of the read-tool optimization.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): cap and dedupe read_app_file to bound context in large apps
read_app_file now defaults to a head slice (1500 lines / 50k chars) with offset/
limit to page further, and skips resending a file whose earlier read is still in
context (per-conversation ledger keyed off the originating tool-call id, so it
self-heals after compaction). Bounds the file-content portion of global-chat
context when working in large raw apps.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ai-evals): add read-heavy raw-app debug case (large data module)
global-test31 induces the model to inspect the 5k-line seedData module, exercising
the read_app_file cap/offset path. Baseline ~262k tokens vs ~200k with the cap+dedupe
change (-24%).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: record A+B benchmark results and fixed-overhead finding
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): clearer read_app_file past-EOF message + unit tests for cap/dedupe
Addresses local-review nits: out-of-range offset now reports 'offset N is past the
end of the file' instead of a backwards 'lines 11-10' label; adds unit coverage for
the slicing (line cap, offset/limit window, char budget, past-EOF) and re-read dedupe
(hit + miss-when-not-retained).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): char-level paging + per-range dedupe for read_app_file
Adds char_offset/char_limit so minified/long-line files can be paged within a line
window, keys the re-read ledger by range (so reading different ranges no longer
collides), and dedupes on the full-file hash (a cached range stub is invalidated
when any byte of the file changes, not just the returned range). Tests updated for
the char-slice behavior plus single-line capping, char paging, and out-of-window
change detection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ai-chat): add read_app_file context micro-benchmark + re-read eval case
Adds a deterministic micro-benchmark (no LLM) that drives read_app_file through a
realistic big-project read pattern (large file, re-read, minified bundle, paging)
and asserts the cap+dedupe cut returned context >50% vs the old whole-file behavior
— isolating the feature's effect from model nondeterminism and guarding against
silent weakening. Adds global-test32, a cross-file consistency investigation that
revisits overlapping files so re-read dedupe is exercised in a real run.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ai-evals): clarify test32 measures the read cap, not dedupe
Verified: sonnet and haiku both read each file once per conversation and retain
it, so test32 never triggers read_app_file re-read dedupe. Dedupe is measured
deterministically by the micro-benchmark instead. Comment corrected to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(ai-chat): drop read_app_file re-read dedupe, ship the cap only
Benchmarking showed the per-conversation re-read dedupe never fires in practice:
across sonnet/opus/gpt-5.5/haiku, every model reads each file once per conversation
and keeps it in context (0 within-conversation re-reads). It was a correct but unused
guard, so this removes the ledger, full-file hash, retention predicate, the
AIChatManager wiring, and the eval decorateHelpers seam — keeping the read cap +
offset/limit/char paging (A), which is the lever that actually bounds context. The
micro-benchmark is now cap-only; test32 is kept as a multi-file read-load case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): add search_app grep tool for global raw-app chat (experimental)
Client-side grep over a raw app's frontend files and inline runnables (literal,
case-insensitive, optional file_glob/context_lines/max_matches, head-capped).
Completes the list -> search -> ranged-read triad. Includes the eval A/B gate
(WMILL_AI_EVAL_DISABLE_SEARCH_APP), unit tests + micro-benchmark, and a
find-all-usages eval case (global-test33).
Experimental: A/B benchmarking shows it is not an unconditional win — it helps
on find-all-usages but adds agentic iterations on navigable apps.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ai-evals): accept search_app as a valid file-inspection tool in raw-app cases
Add requiredToolsAnyOf alternatives-group to ToolValidationSpec and switch
global-test29..32 to it so a model that locates files via search_app instead
of read_app_file no longer false-fails the tool assertion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: remove stale ai-chat context-optimization planning doc
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(ai-chat): drop read_app_file char paging for a hard char cap
The char_offset/char_limit params guarded minified files (a single line over
the char budget) but were effectively unused in benchmarks. Remove them and the
in-window char paging; keep the hard 50k-char budget and, when a read hits it,
tell the model to narrow the line limit (or treat the file as unreadable if a
single line exceeds the budget). Proper long-line handling is left as a TODO.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(ai-chat): bake search_app context to 1 line, clarify query is literal
Drop the context_lines param (models varied it to little effect) for a fixed
SEARCH_APP_CONTEXT_LINES=1, and cap on matching lines instead of pushed rows so
max_matches stays accurate with context always on. Sharpen the query description
to state it is a literal (non-regex) substring and to suggest the call form
(e.g. formatCurrency() to hit call sites and skip formatCurrencyPrecise.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(ai-chat): widen baked search_app context to 2 lines
Models that set the old context_lines param leaned to 2; match the lean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): count every file with a match in search_app header
Move fileHadMatch ahead of the render cap so files whose matches fall past max_matches are still counted (with a regression test). Also swap the raw NUL globstar sentinel for a printable escape (the NUL bytes made core.ts read as binary to grep) and reword two comments to describe current constraints instead of drafting history.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): drop redundant input echoes from app tool results
read_app_file and search_app no longer prefix results with the tool name or echo back the caller's own inputs (file path, query, file_glob) — the model already has them from the call args, and the unbounded query echo could push the search result past its output budget. Keeps the useful signals (line range, match/file counts, truncation) and the actionable advice. Also reword max_matches to 'matching lines' since it caps lines (each expands to context rows). Unit tests updated to the new format.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
draft.value is a json column (not jsonb), so a client could store a U+0000
escape in it. Any later text extraction (`->>` / `to_jsonb`) on such a value
raises 22P05 "unsupported Unicode escape sequence" — one poisoned draft 500'd
the whole GET /drafts/list, silently hiding the home-page "This workspace has N
drafts" banner (and breaking the global drafts page).
Prevent it at the source: sanitize the value in update_draft (the only path that
writes client-supplied draft content) so a NUL never reaches the column.
strip_json_nul does a single backslash-parity-aware byte pass that removes real
NUL escapes (values and keys alike) while leaving a legitimate escaped backslash
intact — O(n) with no serde_json::Value tree to allocate, important because the
slow path is also hit by any value legitimately containing the text after a
backslash (e.g. script source). The clean path is a single substring check.
A SQL migration scrubs rows written before this, gated to genuinely-poisoned
rows (a real NUL makes value::jsonb raise, distinguishing it from a legitimately
escaped backslash). With the data clean, no read-side query needs to change.
Tests: unit tests for the strip helper (escaped-backslash no-op, real+literal
collision, odd-backslash-run parity, nested keys/values) and an integration test
that POSTs a NUL-bearing draft and asserts it is stored and listed NUL-free
(fails without the strip).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(python): split PIP_TRUSTED_HOST by whitespace for multiple hosts
When PIP_TRUSTED_HOST contains multiple space-separated hostnames, the
whole string was passed as a single --trusted-host argument
(--trusted-host "host1 host2") rather than one flag per host. This
matches pip's documented PIP_TRUSTED_HOST convention by emitting a
separate --trusted-host for each host, mirroring the existing
pip_extra_index_url handling.
Fixes WIN-2077
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(python): use shell word-splitting for nsjail trusted-host args
Address review feedback: the previous tr/sed pipeline only split on
single spaces, diverging from the Rust split_whitespace() paths. Repeated
or leading/trailing spaces produced empty --trusted-host flags and
tab-separated hosts were not split. Use an unquoted for-loop over
$TRUSTED_HOST so the shell's own IFS word-splitting handles arbitrary
whitespace and skips empty fields, matching the non-nsjail paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The workspace_id column on unique_ext_jwt_token (migration 20260409145556)
has no FK constraint on the workspace table, and delete_workspace did not
remove its rows. Deleted workspaces left orphaned external JWT token records
that kept appearing in the superadmin External JWTs listing.
Add a DELETE FROM unique_ext_jwt_token WHERE workspace_id = $1 alongside the
other per-table cleanup statements in delete_workspace.
Fixes WIN-2078
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The script_trigger table (migration 20260423050000_script_trigger)
relied on ALTER DEFAULT PRIVILEGES to grant access to windmill_user and
windmill_admin. Those default privileges only apply to objects created
by the role that set them (migration 20250205131523), so deployments
whose migration runner is a different role leave script_trigger
ungranted.
Direct application writes run as the invoking role and fail with
"permission denied for table script_trigger" — notably
clear_script_triggers and insert_script_trigger in
windmill-common/src/assets.rs during every script save.
Add an explicit GRANT on script_trigger and its sequence, matching the
notify_event fix (#9665) and the asset table precedent.
Fixes WIN-2076
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): ignore hash/assets in script diffs and drafts
The script editor's draft value is seeded from the full `getScriptByPath`
DB row (since #9351), so it carries `hash` (the deployed version's
identity) and `assets` (re-derived from the script content by the
editor). Neither is editable draft content, yet both were persisted into
the draft row and surfaced as spurious changes in the workspace/fork
compare diff view.
- Add `hash`, `assets`, and the read-time-computed `inherited_labels` to
`CLEANED_VALUE_KEYS` so the shared diff/unsaved-change strip ignores
them everywhere (DiffDrawer + WorkspaceItemDiffViewer).
- Strip `hash` and `assets` from script drafts at the single persistence
chokepoint (`UserDraftDbSyncer.save`) so every path — reactive
autosave, Ctrl/Cmd+S flush, the pagehide keepalive — sends the same
trimmed payload. On reload the deployed row re-supplies them.
Fixes WIN-2071
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(frontend): clarify draft sanitizer vs diff-strip relationship
The two field lists are intentionally not equal — only the hash/assets
overlap must stay consistent. Reword the comment so a future maintainer
doesn't add keys to one expecting parity with the other.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ansible scripts previously lacked the AI assistant and the contextual
variable helper that other scripting languages expose in the script editor.
- Add 'ansible' to SUPPORTED_CHAT_SCRIPT_LANGUAGES so the AI chat button
shows in the editor toolbar and the AI chat opens in SCRIPT mode without
the "language not supported" warning.
- Add an Ansible system prompt (system_prompts/languages/ansible.md) plus a
LANGUAGE_METADATA entry, and regenerate the auto-generated prompts/skills
so the AI has tailored Ansible context.
- Show the contextual variable picker for ansible and insert references as
`{{ lookup('env', 'NAME') }}`, matching how Windmill exposes reserved
variables as environment variables to the ansible-playbook process.
Fixes WIN-2072
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop empty/unknown `type` values (Windmill emits `type: ""` for untyped fields) and infer `type: array` for nodes carrying `items`, so generated MCP tool schemas validate against JSON Schema draft 2020-12. Anthropic's tool registration rejected the whole tool list otherwise.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: trigger flow error handler on unrecoverable (OOM/zombie) step failures
When a worker is OOM-killed mid-step, the zombie job handler fails the step
via handle_job_error with unrecoverable=true. update_flow_status_after_job_completion
had `false if unrecoverable => false`, which silently completed the flow with the
error and skipped the flow's failure module (error handler). It would also have
pinned the failure module to the dead worker via same_worker.
Unrecoverable failures now route to the failure module instead of being retried or
silently dropped, and the error-handler step is pushed as a regular queued job that
any live worker can pick up.
Fixes WIN-2070
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: skip retry on unrecoverable flow failures, add retry-skip regression test
Address review: the failure-module-on-unrecoverable change must also bypass the per-step retry policy in push_next_flow_job, otherwise an OOM/zombie-killed step with a retry config would be retried instead of routing to the error handler. Gate the retry evaluation on !unrecoverable and add a regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: add sqlx offline cache for new flow-step zombie test query
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: route unrecoverable continue_on_error step failures to the error handler
Addresses Codex/Pi review (P1): with continue_on_error on the failed step, the
step counter was advanced before the unrecoverable decision branch, so
push_next_flow_job pushed the next normal step instead of the failure module —
hiding the worker death and letting the flow complete successfully.
- Do not advance the step counter (inc) for an unrecoverable continue_on_error failure.
- Let the Failure arm in push_next_flow_job route to the failure step even on a
continue_on_error module when unrecoverable.
- Add a regression test (a[continue_on_error] -> b + failure_module): asserts the
failure module runs and step b does not.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(backend): auto-reconnect postgres trigger listener with backoff
The Postgres trigger listener permanently disabled itself on any
connection error (stream close, receive error), so a transient network
interruption (e.g. a cloud provider maintenance window) permanently
killed the trigger.
Restructure the listener to match the Kafka trigger: the replication
connection is now established inside an outer reconnect loop in
`consume`. On a dropped stream or receive error it backs off 30s and
reconnects instead of disabling, reporting a critical error every 10
failed attempts and a recovery once it reconnects. Disabling is kept
only for unrecoverable misconfiguration (missing publication or
replication slot, unparsable replication message).
Fixes WIN-2073
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(backend): count postgres reconnects on stream drop and alert from first
Adopt the SQS listener's reconnection accounting in the Postgres trigger
listener. A stream close or receive error now counts toward the retry
counter and raises a throttled critical error (on the first occurrence,
then every 10 attempts), and the retry counter is reset / recovery is
reported only once the reconnected stream actually delivers a message.
Previously the inner-loop disconnect branches reset `tries` to 0 on every
successful (re)connect and never alerted, so a stream that connected and
then immediately dropped could ping-pong every 30s indefinitely without
ever raising an alert. Resetting on real progress rather than on a bare
connect closes that blind spot and matches the SQS pattern.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The notify_event table (migration 20260203172950_polling_based_events)
relied on ALTER DEFAULT PRIVILEGES to grant access to windmill_user and
windmill_admin. Those default privileges only apply to objects created
by the role that set them (migration 20250205131523), so deployments
whose migration runner is a different role leave notify_event ungranted.
Trigger inserts were already worked around with SECURITY DEFINER
(migration 20260206060555), but direct application inserts that run as
the invoking role still failed with "permission denied for table
notify_event" — notably clear_static_asset_usage in assets.rs during
script save, and restart_worker_group in settings.
Add an explicit GRANT on notify_event and its sequence, matching the
existing explicit-grant pattern used for the asset table.
Fixes WIN-2074
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The raw-app editor keyed its autosave handle on a non-reactive `path`
`let`. SvelteKit does not remount the page on same-route navigation, so
the post-deploy `goto` (draft_{uuid} → chosen path) left the handle stuck
on the old draft slot. Edits to the just-deployed app then autosaved to a
dead key, so autosave appeared broken. Key on the reactive
`page.params.path` instead, matching /scripts/edit.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The home list's draft user badges come from list_apps' `draft_users`
subquery, which only matched `draft.typ = 'app'`. `app` and `raw_app`
are separate draft kinds over the one `app` table, so a deployed raw app
with a pending draft had `is_draft = true` (the join already matches both
kinds) but an empty `draft_users` — the row showed a "Draft" badge with
no owner badge. Match `typ IN ('app', 'raw_app')`, consistent with the
`is_draft` join and the draft-only query.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
finishAppDraftWrite returned `item: result.item`, whose `value` is the entire
app draft (every frontend file body and inline runnable). Each write_app_file /
patch_app_file / write_app_runnable therefore re-sent the whole app back to the
model; on a large app a few edits overflow the 200k context window.
This restores #9530 (which removed the echo) — the DB-backed-draft refactor
(#9601) reintroduced it by routing all app writes through this shared helper
with `item:` re-added. Write results now return only `{ success, message }`,
matching the flow write tools. Adds a regression test asserting the value is
not echoed.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record finalContextTokens per attempt: the input-token total of the last
model request (input + cache-creation + cache-read), i.e. how full the
context window ended up. Complements the cumulative tokenUsage.prompt,
which conflates context size with loop-iteration count.
Captured generically in the shared frontend runEval via the chat loop's
lastIterationUsage, so it covers all frontend modes (global/flow/script/
app), plus CLI mode via the last assistant turn's usage. Aggregated as
average and max over passed attempts and printed in the run summary.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: add workspace asset graph view
Workspace-wide canvas of assets and their producer/consumer scripts,
reachable from the assets page. Left-to-right layered layout via
d3-dag sugiyama, rendered with @xyflow/svelte (same stack as the
flow editor). GET /w/:ws/assets/graph returns deduped nodes + edges.
Follow-ups: filters (kind/folder/search), node detail drawer, inline
script edit from a clicked node.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* all
* all
* all
* update
* all
* all
* all
* feat(pipeline): output-kind picker and per-(lang, output) templates
Add a third stage to PipelineInsertMenu that asks what kind of asset the
new script will produce (datatable / ducklake / s3 parquet / s3 object /
none). The picked kind drives a real wmill SDK skeleton — typed
datatable inserts, ducklake CREATE+INSERT, s3 parquet COPY, etc. — with
the upstream asset auto-wired as the input source when added from an
asset node. Reorder languages to bun → duckdb → python → sql so
data-shaped languages surface first.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* all
* chore(main): release 1.693.4 (#8994)
* chore(main): release 1.693.4
* Apply automatic changes
---------
Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
* feat: ansible delegate_to_git_repo install_requirements, dynamic fields, --limit (#8997)
* feat: ansible delegate_to_git_repo install_requirements, dynamic fields, --limit
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: include .yaml variants in collections/roles requirements lookup
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(cli): only preserve case for raw-app runnableIds, not app/flow summaries (#9000)
* fix(cli): only preserve case for raw-app runnableIds, not app/flow summaries
PR #8940 stopped lowercasing in sanitizeForFilesystem to fix#8939, where
a raw-app runnableId like CamelCaseTSRunnable produced a CamelCase YAML
metadata file but a lowercased code file, making them desync and
register as duplicate runnables on push.
That fix overshot. sanitizeForFilesystem is also reached by
newPathAssigner, which serves normal apps and flows where the input is
the script's human summary ("Get Users Data") rather than an identifier.
There the on-disk filename is the only artifact — there's no companion
YAML to keep in sync — so lowercasing was the right behavior. Removing
it changed both the on-disk filename and the !inline reference in
app.yaml / flow.yaml from get_users_data.inline_script.ts to
Get_Users_Data.inline_script.ts on the next pull, surfacing as
unwanted case churn for users updating to 1.693.x.
Add a preserveCase option to sanitizeForFilesystem (default false →
lowercase). newRawAppPathAssigner opts in; newPathAssigner stays on
the default. Update unit tests accordingly and add an end-to-end
raw-app round-trip in raw_app_sync.test.ts that pushes a CamelCase
backend runnable, pulls it back, and asserts both YAML and code file
preserve case with no lowercase orphan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(cli): use readdir for exact-case orphan check on Windows
The CamelCase round-trip test used fileExists("camelcasetsrunnable.ts")
to assert no lowercase orphan was produced, which false-positives on
Windows since the filesystem is case-insensitive and resolves the
lookup to the existing CamelCaseTSRunnable.ts. Switch to readdir +
toContain so the exact on-disk casing is compared identically on Linux
and Windows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cli): wmill-lock.yaml auto-fill + --rehash-only + path-prefix dedup (#8978)
* fix(cli): canonical lockfile hashes + lock upgrade migration to v3
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(cli): use __app_hash subpath in rehash missing-entry check
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(cli): run sync pull lockfile auto-fill regardless of changes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* chore: regenerate system prompts for new lock and rehash-only commands
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(cli): address review feedback on lock upgrade
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(cli): drop v3 marker; always run fallback; fail-fast on unknown lockfile version
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(cli): drop yaml-round-trip legacy hash variant; recover via --rehash-only
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(cli): include legacy hash in script push staleness warning check
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* revert(cli): drop canonical hash formula; keep raw-bytes hashing
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* perf(cli): reuse change-tracker map for sync pull lockfile auto-fill
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(cli): address review feedback on rehash-only
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test(cli): pin lockfile hash + yaml format and cover regression cases
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test(cli): byte-stable snapshot tests for flow.yaml format
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test(cli): add app and script-metadata yaml snapshot fixtures
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(cli): address claude review on rehash-only
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(cli): factorize script-path to remote-path derivation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(cli): address claude + cubic review (dry-run mutation, rehash short-circuit)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(cli): make rehash a subcommand and factorize fs walks
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(cli): normalize line endings in yaml snapshot tests for windows ci
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(cli): address review feedback on rehash + auto-fill
- Flat-layout scripts now clearGlobalLock before rehash write so legacy
./-prefixed duplicates get cleaned up (matches flow/app behavior).
- Add MalformedLockfileError; sync pull auto-fill re-throws it alongside
UnknownLockVersionError instead of silently warning + continuing.
- Document the legacy step-removal false-negative in
isFlowDirectlyStale / isAppDirectlyStale and the categorizeLocalFiles
ignore-filter invariant.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix: use otel.status_message for OTLP Status.message on failed jobs (#8995)
tracing-opentelemetry only recognizes otel.status_code and
otel.status_message as fields that map to the OTLP Status proto.
The previously-used otel.status_description fell through to the
generic attribute recorder, leaving Status.message unset and
preventing OTLP consumers from filtering spans on error status.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: route email trigger path through standard info channel (#8996)
* docs(skill): document email triggers and S3 attachments
Add an "Email triggers" section to the triggers skill covering the
local-part config, the parsed_email/raw_email/email_extra_args payload,
the URL-style extras convention, where to find trigger_path (only with
a preprocessor, at event.trigger_path), and — most importantly — that
binary attachments are uploaded to the workspace S3 bucket and surface
as `{ s3: "windmill_emails/<job_id>/attachments/<filename>" }`. Scripts
must use wmill.loadS3File / wmill.load_s3_file to read them.
Also pulls EmailTrigger into the schema mappings so a real
`email_trigger.schema.yaml` is generated, and adds Email/Azure to the
trigger kinds list in the CLI agent guidance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref for email trigger path fix
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 26184ab7a4aadfc529dcedf038aa08d36c7ad381
This commit updates the EE repository reference after PR #553 was merged in windmill-ee-private.
Previous ee-repo-ref: 318a46897a605dc9be3817901f35ba5a99a0a525
New ee-repo-ref: 26184ab7a4aadfc529dcedf038aa08d36c7ad381
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* update git sync version to 1.693.5
* fix: pair PG arg type with actual Rust binding to keep query_typed_raw safe (#8999)
* fix: pair PG arg type with actual Rust binding to keep query_typed_raw safe
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(pg): wrap encoder errors with arg context, add fallback test
Followups on #8999 review:
- Wrap rust-postgres "error serializing parameter N" failures with the arg
name, JSON value kind, and asserted Postgres type plus a hint about an
explicit cast — so users see actionable context instead of an opaque
WrongType.
- Drift-prevention meta-test: assert otyp_to_pg_type and convert_val agree
on the Type for every recognised arg_t when the JSON value matches its
natural Rust kind. Catches future drift if either side changes.
- Integration test for the prepare + query_raw fallback path: confirms
unrecognised arg_t (custom enum) is routed through prepare and the
server-resolved type appears in the failure surface — flips into a
test failure if a regression accidentally routes unrecognised types
through query_typed_raw.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(pg): add otyp_inferred flag + regex-based placeholder renumbering
Two follow-ups from the review of #8999:
1. **Issue #1 (Number/Bool + explicit text decl in WHERE)**
Add `Arg::otyp_inferred: bool` to the parser. The PG SQL parser sets
it `true` only at the "no info → fall back to text" site (bare `$N`,
no inline cast, no `-- $N (TYPE)` decl). All other arg sources keep
it `false`.
In `convert_val` this flag distinguishes:
- explicit text-like target (`-- $1 (text)` or `$1::text`) — coerce
`Bool`/`Number` → `Box<String>` so `WHERE text_col = $1` works
(`text = text` operator). Pre-#8988 behaviour, restored.
- parser-default text (bare `$N`) — bind the value's natural Rust
type so the regression case (`Value::Bool` against a real `bool`
column via `CAST AS bool`) keeps working.
`Arg` is in `windmill-parser`; the new field has `#[serde(default)]`
so persisted signatures stay backward-compatible.
2. **Issue #4 ($5/$50 substring rewrite collision)**
Replace the per-index `String::replace` chain (which turned `$50`
into `$10` when oidx=5 was processed first) with a single regex
pass. `\d+` is greedy, so `$5` and `$50` match as distinct units;
indices outside the mapping are left intact.
3. Tests:
- parser: `test_parse_pgsql_otyp_inferred_flag` covers bare/inline-
cast/decl/mixed shapes.
- executor unit: `convert_val_bool_against_every_arg_t` and
`convert_val_*_number_*` split each text-like target into explicit
vs inferred expectations.
- executor unit: `renumber_sparse_placeholders_no_collision`.
- integration: `test_postgresql_arg_type_combinations` adds 4 cases
covering decl(text)+Number/Bool in WHERE, bare $1+Bool, and
sparse positional args ($5/$50).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(pg+sdk): enum support, extended String arms, position-aware $N rewrite, SDK quality
Backend:
1. **`AnyTextValue` ToSql/FromSql wrapper**: vanilla `tokio_postgres`'s
`ToSql for String` / `FromSql for String` reject `Kind::Enum` and
`Kind::Domain` even though the wire format is plain UTF-8. The wrapper
accepts those kinds in both directions. End result: explicit
`$1::my_enum` / `CAST($1 AS my_enum)` casts now round-trip without the
ugly `CAST($1::text AS my_enum)` workaround, AND `SELECT enum_col`
results come back as JSON strings instead of erroring at the FromSql
layer.
2. **#10 — Value::String → numeric/real/double/oid/bool**. Without these
arms, a string-encoded value (`"3.14"`, `"true"`) for a non-text /
non-temporal arg_t fell through to `Box<String> + TEXT`, which then
failed at the server (no implicit cast text→numeric in expression
context). Now strings are parsed into the matching native type with
clear error messages on parse failure.
3. **Position-aware `$N` rewrite**: replaces the regex-based renumbering
(which fixed the `$5/$50` substring collision but still walked through
string literals and comments, mangling `'price: $5'` etc.) with a
walk over `parse_pg_statement_arg_positions` — the same
string/comment/dollar-quote-aware tokenizer used for index discovery.
Adds `parse_pg_statement_arg_positions` to the parser's public API.
SDK:
4. **BigInt support**: `JSON.stringify(BigInt)` throws. The SDK now
stringifies bigints before serialisation; the executor accepts
numeric strings into BIGINT arg slots via the existing
`Value::String → INT8` parsing arm. SDK-side `inferSqlType` is split
so `BigInt` always resolves to `BIGINT` (was reaching
`Number.isInteger(BigInt)` which returns false → wrong default).
5. **Homogeneous array auto-tag**: `${[1,2,3]}` against an `int[]` column
now emits `$1::BIGINT[]` instead of `$1::JSON`. Detection covers
primitive types only (number / bigint / string / boolean); mixed or
nested arrays still fall back to JSON. Mixed int/float widens to
`DOUBLE PRECISION[]`.
6. **`.query()` positional bug**: previously the `.query()` method
abused the template-tag builder, which appended `$N::TYPE` after the
user's literal SQL string instead of binding by position
(`SELECT $1, $2` became `SELECT $1, $2$1::BIGINT`). Now `.query()`
builds the executor-shaped content directly: a `-- $N argN (TYPE)`
declaration block followed by the user's SQL verbatim.
Tests:
- Parser: `test_parse_pg_statement_arg_positions_skips_strings_and_comments`
asserts string literals, comments, and dollar-quoted blocks don't
produce positions (so renumbering doesn't mangle them).
- Executor unit: `renumber_sparse_placeholders_no_collision_no_string_mangling`
uses the new position-aware path and includes string-literal + comment
+ `$$…$$` cases. Existing convert_val tests grow to cover new
String→numeric/real/double/oid/bool arms.
- Integration: `test_postgresql_arg_type_combinations` adds 13 cases
(enum round-trip both directions, string→numeric/real/double/bool/oid,
string-literal `$N` non-mangling). The prepare-fallback test now
asserts SUCCESS (not failure) for enum encoding via AnyTextValue.
- SDK: new `typescript-client/tests/sqlUtils.test.ts` (42 tests)
exhaustively covering inferSqlType primitives + arrays,
parseTypeAnnotation, datatable() template tag (with all the new
shapes — BigInt, homogeneous arrays, RawSql, schema preamble),
datatable().query() positional, and ducklake() shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(pg): replace DISCARD ALL with curated reset (preserves typeinfo cache)
Found while exhaustively probing custom-type DX: every cached-connection
reuse was running `DISCARD ALL`, whose included `DEALLOCATE ALL`
deallocates *all* prepared statements server-side — including the typeinfo
statements that tokio_postgres caches per-Client to resolve custom enum /
domain Oids. tokio_postgres still held `Statement` objects whose names
the server had forgotten, so the next custom-type query failed with
intermittent "prepared statement \"sN\" does not exist" errors. The
failure was easy to reproduce: any sequence that forced typeinfo lookup
for two different custom-type kinds on the same cached connection (e.g.
enum followed by domain) would hit it.
Replace `DISCARD ALL` with a curated reset that explicitly targets the
state we actually care about, *without* touching prepared statements:
RESET ALL — GUC parameters (search_path, application
_name, statement_timeout, …)
RESET SESSION AUTHORIZATION — undoes both `SET SESSION AUTHORIZATION`
and `SET ROLE` (RESET ALL does NOT —
these aren't GUC parameters, so without
this an elevated role from a previous
job would silently leak)
UNLISTEN * — drops LISTEN registrations
CLOSE ALL — closes open cursors
Trade-off: temp tables, advisory locks (session-scoped), and user-created
PREPARE statements may persist across cached-connection reuse — rare in
datatable / PG-script workloads. tokio_postgres's typeinfo cache survives
intact, so custom enum / domain queries are fast on subsequent reuse.
Tests:
- `test_postgresql_custom_types_on_cached_connection` — runs 10×
alternating enum + domain queries on a cached connection. Pre-fix this
failed with `prepared statement "sN" does not exist` after the first
reuse; post-fix passes.
- `test_postgresql_set_role_does_not_leak_across_cached_connection` —
switches `SET ROLE` and `SET SESSION AUTHORIZATION` to a non-postgres
role, then runs a follow-up job and asserts current_user/session_user
are restored. Specifically catches the case where someone might switch
back to `RESET ALL` alone (which doesn't cover SET ROLE / SESSION
AUTHORIZATION) and silently introduce a permission-leak vector.
- All existing session-isolation tests
(`test_postgresql_cached_connection_resets_session`,
`test_postgresql_single_worker_session_isolation`,
`test_postgresql_100_jobs_cached`) continue to pass.
Found via end-to-end probing of datatable / PG-script DX, not previously
covered: the existing isolation tests only did `SET ROLE postgres`, the
connecting user, so the leak was invisible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(pg): address PR #8999 review (cubic + claude)
cubic (P1, real bug):
- `convert_vec_val` for `timetz` array asserted `Type::TIMETZ_ARRAY`, but
chrono `NaiveTime` only encodes for TIME (same caveat as the scalar
arm). Switch to `Type::TIME_ARRAY`; rely on PG's implicit `time→timetz`
assignment cast at the column site. Add an explicit unit test.
claude (#1, silent failure → explicit error):
- `Bool` + explicit `(char)` / `(character)` decl previously silently
bound BOOL, hoping the server would cast at the use site — but PG has
no implicit `bool→char` and the resulting error
("operator does not exist: bool = char") was opaque. Now error at
bind time with an actionable hint to use `bool` decl or pass the
value as a "t"/"f" string.
claude (#2, asymmetry doc):
- Object/Array still coerce to text on `matches!(typ, Typ::Str(_))`
(covers both explicit AND inferred-default text), unlike Bool/Number
which key on `explicit_text_target`. The asymmetry is intentional
(no implicit `jsonb → text` cast in expression context vs PG having
implicit `bool/int → text` casts) — added a body comment so future
maintainers don't try to "align" them.
claude (#3, perf):
- `parse_pg_statement_arg_indices` and `parse_pg_statement_arg_positions`
walked the SQL tokenizer twice. Fold into a single pass that derives
the index set from the position list.
claude (#4, fmt drift):
- `cargo fmt` over the parser crates I touched with perl scripts in the
earlier commit (windmill-parser-{sql,bash,ts,go,php,java,csharp,nu,py,
rust,graphql,yaml,r}). Net cosmetic.
claude (#5, parseTypeAnnotation):
- One-line caveat in the SDK's `parseTypeAnnotation` that the returned
string is presence-only (e.g. `${x}::DOUBLE PRECISION` returns
`"DOUBLE"`, `CAST(${x} AS int)` returns `"int)"` — neither matches a
real PG type, but the only consumer just checks `!== undefined`).
While here — discovered + fixed independently while exhaustively probing
DX:
- **Replace `DISCARD ALL` with curated reset** (`RESET ALL; RESET
SESSION AUTHORIZATION; UNLISTEN *; CLOSE ALL;`). DISCARD's
`DEALLOCATE ALL` killed tokio_postgres' typeinfo cache, producing
intermittent `prepared statement "sN" does not exist` errors on
custom-type queries after cached-conn reuse. New regression tests:
`test_postgresql_custom_types_on_cached_connection` and
`test_postgresql_set_role_does_not_leak_across_cached_connection`
(the latter catches the case where someone might switch back to
`RESET ALL` alone and silently introduce a permission-leak vector —
RESET ALL doesn't cover SET ROLE / SET SESSION AUTHORIZATION).
- **ISO-8601 timestamp results** (`pg_cell_to_json_value`). Pre-fix
`TIMESTAMP` was rendered with a space separator ("2024-01-15 10:30:00")
and `TIMESTAMPTZ` with " UTC" suffix ("2024-01-15 10:30:00 UTC") —
neither parseable by `date-fns parseISO`, JavaScript `new Date()` is
lenient enough to handle them but several frontend `App*Input.svelte`
components use parseISO and fail silently. Switched to ISO-8601 with
`T` separator and `+00:00` offset; arg-parsing path still accepts the
legacy " UTC" suffix for back-compat.
Test coverage:
- 17/17 unit (`pg_executor::tests`)
- 9/9 integration (`backend/tests/worker.rs`, `test_postgresql_*`)
- 27/27 parser (`windmill-parser-sql`)
- 42/42 SDK (`typescript-client/tests/sqlUtils.test.ts`)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(pg): bounded one-shot warning on numeric precision loss + ISO-8601 + NaN handling
Found while probing PG-script DX with millions of numeric cells:
1. **Numeric precision-loss warning**: `numeric` results are still serialised
as JSON Number (back-compat — switching to JSON String would silently
break user code doing arithmetic on results), but we now detect
`Decimal -> f64 -> Decimal` round-trip failure and emit a single
job-log warning recommending a `::text` cast in the SQL. Bounded by
`NUMERIC_PRECISION_CHECK_BUDGET = 256` cells per query (one atomic
load + one fetch_sub on the hot path; first lossy value
short-circuits to a single load thereafter). Worst-case overhead on
a 1M-cell numeric-heavy query: ~25µs of checks + 5ns × N atomic
loads (vs. ~100ms unbounded).
2. **ISO-8601 timestamps**: `pg_cell_to_json_value` previously returned
`"2024-01-15 10:30:00"` (TIMESTAMP) and `"2024-01-15 10:30:00 UTC"`
(TIMESTAMPTZ) — neither parseable by date-fns `parseISO`, which is
what the apps `App*Input.svelte` components use, so timestamp values
silently failed to round-trip into date pickers. Switch to ISO-8601
(`T` separator + `+00:00` offset) on the result side; arg-parser
continues to accept the legacy `" UTC"`-suffixed format for
back-compat.
3. **Float NaN / Infinity results**: `Number::from_f64` returns None for
NaN / ±Inf, which `pg_cell_to_json_value` was raising as
"invalid json-float" — failing the *entire* query if any cell held
one of these special values. Now serialise them as JSON strings
("NaN", "Infinity", "-Infinity") and let the rest of the row come
through. Arg-side: `s.parse::<f64>()` already accepts the same
strings.
Tests:
- `decimal_fits_f64_losslessly_predicate` — covers fits / doesn't-fit
cases for the precision-loss predicate.
- `precision_check_budget_caps_per_query_overhead` — locks in the
budget cap and the loss-flag short-circuit.
- All 9 PG integration tests + 17 unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(pg): add pg_advisory_unlock_all to reset; warn on missing args; honor decl defaults
While probing PG-script DX further found three more frictions:
1. **Advisory lock leak** (cubic P2): switching from `DISCARD ALL` to
`RESET ALL; RESET SESSION AUTHORIZATION; UNLISTEN *; CLOSE ALL;`
meant session-scoped advisory locks (`pg_advisory_lock`) leaked
across cached-connection reuse. Add `SELECT pg_advisory_unlock_all()`
to the chain — `DISCARD ALL` covered this implicitly via
`DISCARD PLANS / DEALLOCATE / pg_advisory_unlock_all` and we lost it
in the switch.
2. **Missing-arg silent NULL**: an arg declared in the SQL (e.g.
`-- $1 amount (numeric)`) but not provided in the args object was
bound as NULL with no error / warning. Misspelling the key in the
args object silently produced a row of NULLs — a notorious DX
debugging trap. Now: collect the names of declared-but-missing
args during dispatch and emit a single one-shot warning to the job
logs at end-of-query naming each one. Bound NULL is preserved for
back-compat.
3. **Declaration defaults ignored**: `-- $1 a (int) = 5` carries
`arg.default = Some(Number(5))`, but the dispatch fell straight to
NULL when the arg was missing. Now: respect the default —
user-supplied value > declaration default > NULL. Also fixes the
warning logic above (only warn for args that *don't* have a default).
Tests: existing 19 unit + 9 integration pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(pg): multi-word PG types with [] suffix lost the array-ness; array arms accept stringified values
Two more frictions found while probing SDK end-to-end against a real
datatable resource:
1. **Multi-word array types lose the [] suffix in the parser**.
`transform_types_with_spaces` recognises aliases for "double
precision", "character varying", "timestamp with time zone", etc.
but its return type was `&'a str` — only the bare alias, never with
a trailing `[]`. The `RE_CODE_PGSQL` regex's `\w+` captures stop at
the first space, so the regex's own `(?:\[\])?` array-suffix branch
sees only `"double"` (not `"double precision[]"`); the `[]` was
silently lost. Result: `$1::double precision[]` (which the SDK now
emits for homogeneous float arrays via the new auto-tag) routed
through `Value::Array → Type::JSONB` and the server failed with
"cannot cast type jsonb to double precision[]".
Fix: switch `transform_types_with_spaces` to return `Cow<'a, str>`
and re-check the trailing bytes after a multi-word match. If they
start with `[]`, return `format!("{alias}[]")` — Owned. Single-word
types and the no-match path keep returning Borrowed slices, so no
allocation in the hot path.
2. **Array arms in `convert_vec_val` rejected stringified values for
numeric / int* / bool / oid / real / double**. The scalar `convert_val`
already parses strings into the matching native type for these arg_ts,
but the array variant only accepted JSON-native counterparts. Sending
`["1.5", "2.5", "3.5"]` against `$1::numeric[]` (e.g. via `unnest` for
bulk loading, or `JSON.stringify(BigInt[])` round-trip) failed with
"Mixed types in array". Now the array arms mirror the scalar ones —
`as_<native>().or_else(|| as_str().and_then(parse))` — so both shapes
round-trip cleanly.
Tests: 19 unit + 9 integration pass; existing parser tests cover the
multi-word array forms (the regex-cap behaviour didn't break for
single-word types, and Cow plumbing is transparent to all callers).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(parsers): add otyp_inferred field to Arg literals in tests + 3 missed src files
CI failures: the perl-driven sweep that added `otyp_inferred: false` to
every `Arg { ... }` literal when I introduced the field in the parser
schema covered `src/lib.rs` files but missed:
- parsers/windmill-parser-bash/src/lib.rs (mass-edited but a
later format pass un-applied a few sites)
- parsers/windmill-parser-go/src/lib.rs (same)
- parsers/windmill-parser-graphql/src/lib.rs (same)
- parsers/windmill-parser-nu/tests/tests.rs (test file — not
swept the first time)
- parsers/windmill-parser-ts/tests/tests.rs (test file — same)
Also tightened the regex to handle `oidx: None` without the trailing
comma (some test files had the field as the last initialiser line).
`cargo build --features <CI feature combo> --workspace --all-targets`
is clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sdk): Date → TIMESTAMPTZ; NaN / ±Infinity → string
Two more frictions found while running the actual SDK end-to-end against
a live datatable resource:
1. **JS `Date`** fell into the typeof "object" branch and was tagged
`::JSON`. It worked accidentally for `${date}::timestamptz` via PG's
`json → text → timestamptz` implicit cast chain, but `${date}` against
a `timestamptz` column without a user-supplied cast bound the value
as a JSON string and the comparison `timestamptz = json` failed. Now:
`inferSqlType` recognises `Date` and tags `::TIMESTAMPTZ`;
`serializeArgValue` emits `Date.toISOString()` so the executor's
`Value::String → TIMESTAMPTZ` arm parses it cleanly.
2. **JS `NaN` / `±Infinity`** silently became NULL. `JSON.stringify(NaN)`
returns `"null"` per the JS spec, so the value reached the executor as
JSON null — the SDK's `::DOUBLE PRECISION` tag then bound a NULL
double. Fix: detect non-finite numbers in `serializeArgValue` and
stringify them as `"NaN" / "Infinity" / "-Infinity"`. The executor's
`Value::String → FLOAT8` arm (`f64::from_str`) accepts these literals
directly, and the result-side already renders the values as JSON
strings (matching round-trip).
SDK unit tests grow from 42 → 44 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(pg): integration coverage for multi-word arrays + stringified array elements
Locks in the two array fixes from the previous commit
(`fix(pg): multi-word PG types with [] suffix lost the array-ness`)
with end-to-end cases in `test_postgresql_arg_type_combinations`:
- `double precision[]`, `character varying[]`, `timestamp without time
zone[]` — verifies the parser keeps the `[]` suffix after multi-word
alias resolution.
- `numeric[]` / `int[]` / `bool[]` from stringified primitives — verifies
the array arms of `convert_vec_val` apply the same string-coercion
the scalar arms do.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style: fix indentation drift on otyp_inferred lines
cargo fmt cleanup of leftover indentation where the perl-driven sweep
that introduced the otyp_inferred field landed at the wrong column.
No behaviour change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: support assigning a worker tag to app inline scripts (#9002)
* feat: support assigning a worker tag to app/raw-app inline scripts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: omit empty tag field from inline script raw_code payload
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* style: shrink tag popover width
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat(pipeline): 2-col picker, draft path edit, save-all + leave guard
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* all
* all
* update
* fix(cli): forward HEADERS env var on every backend fetch call (#9075)
Several `fetch()` callers in the CLI bypassed `OpenAPI.HEADERS` and skipped
the `HEADERS` env var, causing requests to fail behind auth gateways like
Cloudflare Access (same shape as #6421):
- `pushScript()` `/scripts/create` and `/scripts/create_snapshot` — regressed
in #8936 when the call switched from `wmill.createScript()` (SDK) to a raw
`fetch` for the `skip_if_noop` query param.
- Script preview `/jobs/run/preview_bundle`.
- App dev `/jobs_u/getupdate_sse` SSE stream.
- `wmill docs` `/api/inkeep`.
All four now spread `getHeaders()` and call `detectAuthGatewayChallenge()`
so a Cloudflare/SSO challenge surfaces a clear error instead of an opaque
JSON parse failure.
Adds `test/headers_env_var.test.ts`: spins up an auth-gateway proxy that
403s requests missing `CF-Access-Client-Id` / `CF-Access-Client-Secret` and
otherwise reverse-proxies to the test backend, then runs `wmill sync push`
of a fresh script through the proxy. Negative case (no `HEADERS` env)
verifies the proxy actually gates; positive case asserts every request
including `/scripts/create` reaches the backend with the headers attached.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cli): add --parallel flag to generate-metadata (#9074)
* feat(cli): add --parallel flag to generate-metadata
* fix(cli): validate --parallel input and harden flush ordering
* perf(flows): skip flow_env DB+transform work when no resolution is needed (#9078)
* fix(cli-tests): stabilize flow lock-gen race + Windows path (#9080)
* fix(cli-tests): stabilize flow lock-gen race + Windows path
Three CLI test failures on the latest main, all flaky on CI:
1. `Mixed Case Paths: pull and push flow with capitalized folder` and
`Integration: Mixed scripts and flows with nonDottedPaths are
idempotent`: flow create/update queues an async FlowDependencies job
that fills inline-script lockfiles and rewrites flow.value. The tests
pulled/pushed before the worker finished, so dry-run idempotency saw
phantom `*.inline_script.lock` adds and `flow.yaml` edits. Added a
`waitForFlowDependencyJob` helper that polls `/flows/get` for the
latest `dependency_job` and `/jobs_u/completed/get` until it lands,
and called it after each API/CLI flow write in both tests.
2. `HEADERS env var is forwarded on every CLI fetch` (Windows-only,
added in #9075): the new test built the CLI entrypoint via
`new URL("..", import.meta.url).pathname`, which yields `/C:/...` on
Windows and `Bun.spawn` rejected before reaching the proxy, leaving
`rejectedRequests.length` at 0. Switched to
`fileURLToPath` + `node:path.join` to match `cargo_backend.ts`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli-tests): use /flows/deployment_status to actually wait for dep job
CI reviewers (Claude, Codex) flagged the prior `waitForFlowDependencyJob`
as a no-op: it read `flow.dependency_job` from `/api/w/{ws}/flows/get`,
but `Flow` / `FlowWithStarred` (backend/windmill-types/src/flows.rs:20-60)
do not include that field. The helper exited on the first iteration
without polling.
Switch to `/api/w/{ws}/flows/deployment_status/p/{path}`, which returns
`{ lock_error_logs, job_id }`. `job_id` is the FlowDependencies UUID
written into `deployment_metadata` in the same tx as the dep-job push
(backend/windmill-api-flows/src/flows.rs:660-672 and :1275-1292), so by
the time the create/update API call returns, the response carries the
latest dep-job UUID. Then poll `/jobs_u/completed/get/{job_id}` as
before. Local runtime for `mixed_case_paths.test.ts` jumps from ~9s to
~32s, confirming the helper now actually waits instead of returning
immediately. The 404 short-circuit in `sync_pull_push.test.ts` still
works — `get_deployment_status` returns 404 when the flow is absent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* perf(flows): cache resolved flow_env per flow execution (#9079)
* perf(flows): cache resolved flow_env per flow execution
* perf(flows): tighten flow_env cache cap to 1024 and clarify memory note
* perf(flows): don't cache transient flow_env resolution failures
* chore(main): release 1.698.0 (#9076)
* chore(main): release 1.698.0
* Apply automatic changes
---------
Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
* fix: reject root-rooted paths in ansible playbook validator on windows (#9081)
* fix(native-triggers): serialize Google channel renewal across replicas (#9060)
* fix(native-triggers): serialize Google channel renewal across replicas
`sync_all_triggers` runs every 5 minutes on every windmill-app replica
with no leader election. Multiple replicas were each rotating the
webhook token, creating a new Google watch channel, and racing the
trigger UPDATE — leaving the loser's new token (in `token`) and channel
(in Google) orphaned. Cloud was accumulating ~5 leaked tokens/week
without the silent best-effort `delete_token_by_hash` ever logging a
warning.
Wrap each per-trigger renewal in a transaction and acquire the row with
`SELECT … FOR UPDATE SKIP LOCKED`. Contending replicas skip the row
instead of duplicating the work. The lock spans `rotate_webhook_token`
→ Google API call → `update_native_trigger_service_config` and is only
released on commit. Re-checks `should_renew_channel` after acquiring
the lock so a replica that committed seconds earlier doesn't trigger a
duplicate renewal.
The pattern matches existing batch-cleanup paths in `monitor.rs`
(job-retention sweep) and other `FOR UPDATE SKIP LOCKED` call sites.
Also logs at `debug!` when `delete_token_by_hash` finds no matching row,
so future investigations can distinguish "deleted" from "not found"
without changing the `Ok(false)` contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fixup! fix(native-triggers): serialize Google channel renewal across replicas
* fixup! fix(native-triggers): serialize Google channel renewal across replicas
fixup! fix(native-triggers): serialize Google channel renewal across replicas
Address claude review:
- #5: per-skip log info -> debug (expected outcome under SKIP LOCKED)
- #2: warn moved out of delete_token_by_hash to the call site that knows the
expected state (try_renew_channel_locked); other callers are race-prone and
shouldn't warn
- #3: NULL service_config now warns (anomalous case)
- #4: post-Google-API DB-update + commit failures log distinctly so the
channel-orphan case is grep-able
Plus: add 14d expiry to Google webhook tokens via ServiceName::webhook_token_expiration,
mint fresh ephemeral-webhook-{service}-{rd5} labels at create + rotate so the
existing 'ephemeral-' filter excludes them from user-token email/critical-alert
paths (no filter changes in 3 places). Orphans now self-clean via the existing
expiry sweep in monitor.rs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fixup! fix(native-triggers): serialize Google channel renewal across replicas
fixup! fix(native-triggers): serialize Google channel renewal across replicas
Address second-round review:
- Claude #1 (P2): username_override_from_label now strips the 'ephemeral-'
prefix for ephemeral-webhook-* labels, so created_by stays
webhook-{service}-{rd5} instead of changing to label-ephemeral-webhook-...
(preserves audit/job-list filter compatibility)
- Codex (P2): updated renew_channel doc — labels are no longer copied; rotate
mints fresh ephemeral-webhook-google-{rd5} with 14d expiration
- Claude #3 (optional): test_rotate_webhook_token now asserts the rotated
Google token has an ephemeral-webhook-google-* label and a populated
expiration
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fixup! fix(native-triggers): serialize Google channel renewal across replicas
fixup! fix(native-triggers): serialize Google channel renewal across replicas
Reconsider the previous fixup: stripping the 'ephemeral-' prefix made
created_by no longer match token.label exactly, defeating the linking
purpose. Just allowlist 'ephemeral-webhook-' alongside the other
recognized webhook/email/ws prefixes — created_by becomes
ephemeral-webhook-google-XXXXX, matching token.label exactly. The
'ephemeral-' substring also informs operators that this is a
system-managed auto-expiring token vs a user-managed webhook trigger.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): bump svelte version in `wmill app new` template (#9084)
* fix(cli): bump svelte version in `wmill app new` template
The svelte5 template pinned `svelte` to `5.45.2`, but the Svelte
compiler bundled in `wmill app dev` emits `$.delegated('click', ...)`
calls. The `delegated` export was added later, so 5.45.2 doesn't have
it — esbuild warns `Import "delegated" will always be undefined`,
replaces the call with `void 0`, and the page crashes at first
event-handler bind (white screen).
Bump to `^5.55.5` so the compiler and runtime stay in sync.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(frontend): bump svelte version in raw_apps UI template
Mirror the CLI fix: the UI's `Add raw app` flow scaffolds a
package.json with `svelte: "5.45.2"`. That works today only because
the bundled rolldown worker also pins 5.45.2 — when the worker is
upgraded past 5.51.1, the compiler will emit `$.delegated()` and the
runtime won't have it, producing the same white-page crash that hit
the CLI.
5.55.5 still exports `event` (used by the current bundled compiler),
so this is forward-compatible: it works with the 5.45.2 compiler now
and won't break when the worker is upgraded.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* perf(flows): gate flow_env resolve on expr text and share cache with handle_flow (#9085)
* feat: parse windmill_failure field to tag run as failure (#9073)
* feat: parse windmill_failure field in job result to tag run as failure
* feat: preserve top-level fields when windmill_failure tags a run as failure
* fix: address review findings on windmill_manual_failure
* refactor: rename windmill_manual_failure to wm_failure and add wm_* aliases
* fix: prefer injected ManualFailure error over sibling name/message in OTel
* fix: hide _ENTRYPOINT_OVERRIDE jobs from script/flow history panel (#9088)
* fix(flows): populate error handler input args from failure picker (#9087)
* fix(flows): populate error handler input args from failure picker
* style(flows): fix indentation in failure-step branch
* fix(python): verify wheel RECORD on cache pull/install, finalize piptar (#9090)
The Python per-package dependency cache could persist an incomplete wheel
extraction with `.valid.windmill` set, then propagate that broken artifact
to every worker through the object store. Customer hit this on
argon2-cffi==25.1.0 (missing argon2/_utils.py), and previously on
botocore/httpx (truncated tars). Symptom is a runtime ImportError that
looks like a missing dependency declaration rather than a Windmill bug.
Three changes that together stop the propagation:
1. After `pull_from_tar`, parse the wheel's `<dist-info>/RECORD` and
confirm every listed path exists on disk before writing
`.valid.windmill`. On failure, wipe the directory and fall through
to a fresh local install — the next install also self-heals the
broken object-store entry by pushing a fresh tar.
2. After `uv pip install` succeeds, run the same RECORD check before
queuing the piptar upload or writing `.valid.windmill`. A bad install
never becomes the source of a broken tar in the object store.
3. Finalize the tar (`drop(tar.into_inner()?)`) before reading its bytes
for upload, so we never push an unfinalized archive (no end-of-archive
marker) to the object store.
Verified with a 60-package end-to-end integration test (first-fill →
clear-local-cache → re-pull-from-objectstore → corrupt-objectstore-tar
→ detect-and-self-heal). All 27 packages on the live test pulled cleanly,
and the deliberately corrupted argon2-cffi tar was caught with the exact
expected log line ("wheel RECORD lists files missing on disk: argon2/_utils.py")
and replaced with a fresh tar.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(main): release 1.699.0 (#9082)
* chore(main): release 1.699.0
* Apply automatic changes
---------
Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
* feat(cli): auto-infer args for `wmill app push` (#9091)
Run `wmill app push` from inside an app folder (e.g. `f/foo/my_app.app/`)
with no args. The local path defaults to CWD, and the remote path is
derived from CWD relative to `wmill.yaml`, with `.app`/`.raw_app`/
`__app`/`__raw_app` suffixes stripped. Either, both, or neither
positional argument can be passed.
Also resolves `file_path` against the user's original CWD before
`resolveWorkspace` may chdir to the wmill.yaml root, so a relative
`file_path` argument is interpreted from where the user invoked the
command (previously it could resolve against the wrong directory).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* all
* fix(pipeline): live-update graph for annotations and body assets
* fix(pipeline): persist draft body edits across node switches
* fix(pipeline): persist live writes per draft to keep output node fresh after switch
* feat(pipeline): animate graph edges only while a runnable is executing
* feat(pipeline): add run button on script nodes + recomputing hint on preview
* feat(pipeline): compact preview layout, two-way Test/Run sync
* fix(pipeline): test button cross-browser placement (no overflow trick)
* style(log-viewer): replace took/mem-peak labels with timer/cpu icons
* style(log-viewer): hyphenate Auto-scroll label and prevent wrapping
* style(log-viewer): lowercase auto-scroll label, force vertical scrollbar
* style(log-viewer): force horizontal scrollbar instead of vertical
* fix(log-viewer): scope overflow-x to top bar so pre doesn't drive panel width
* fix(pipeline): overlay live body-asset writes for persisted scripts too
* fix(pipeline): persist inferred body assets at save so edges survive page reload
* fix(pipeline): snapshot live draft writes at persist time so they survive reload
* fix(pipeline): keep inferred body writes on the canvas across selection changes
* fix(pipeline): untrack inferredWrites cache mutation to break effect loop
* fix(pipeline): refetch asset graph after persisted-script save
* feat(pipeline): optional AI prompt when creating a pipeline script
* all
* all
* test: cover asset-trigger dispatch end-to-end through worker
* feat(pipeline): split-button Test with optional downstream cascade
* feat(pipeline): cascade option on graph Run + match button heights
* style(pipeline): match caret bg/text to Test button's accent-secondary
* feat(pipeline): split Run pill on graph node exposes cascade option
* feat: live run activity + status badges in pipeline asset graph
- folder-scoped queue poll lights up the downstream asset-trigger
cascade (not just the launched script); zero requests at rest,
catch-up for fast hops, auto-disarm when idle
- per-runnable node badge: last-run status + session run count
- animate unsaved/live-parsed edges (was unconditionally suppressed)
- background-pane click no longer clears selection
- run-bridge guarded so node selection/save no longer triggers a test
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: live activity log, optimistic badges, node-avoiding graph edges
- collapsible folder activity log (PipelineEventLog): live job feed,
polls only while open/active, slow idle cadence, capped + pruned
- composable: observe mode + events list + run-count anchored to
graph-open time (pre-existing history excluded)
- optimistic node badge: launched script shows running instantly via
the zero-latency activeRunnable hint, keeps the polled run count
- activity pane height capped (min(18rem,40vh)) then scrolls
- route asset-graph edges through sugiyama-computed waypoints so they
go around nodes instead of under them; bezier fallback for
adjacent-layer / draft-overlay edges
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: prefetch all folder script assets so graph is stable on load
On pipeline load, eagerly infer body assets for every persisted folder
script and seed the existing inferredWritesByPath overlay, instead of
only filling it when a node is selected. Scripts whose persisted asset
rows are missing (e.g. object-form writeS3File) now have their edges
from first paint, so clicking a node no longer re-layouts the graph.
One-shot per (workspace, base-graph) load, untracked map reads,
generation-cancelled, pool-capped fetches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* perf: guard no-op poll re-layout; dedupe write-asset extraction
- skip reactive ids/states/events reassignment when unchanged, so an
idle poll tick no longer re-runs the full sugiyama layout every 3-6s
- bound countedJobIds (rebuilt from eventsById in lockstep with prune)
- extract shared extractWrites() helper, replacing 4 copy-pasted
write-asset filter/map blocks in the pipeline page
- compute activeRunnable node-id once, reuse for the active-edge set
and the optimistic badge (flattened ternary); trim narrating docs
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: live read-lineage overlay for inferred body assets
Renaming e.g. duckdb read_parquet('s3://...') / loadS3File now updates
the asset->reader edge live instead of only after Save re-derives the
persisted asset rows.
- extractReads() (+ shared refsByAccess) mirroring extractWrites
- inferredReadsByPath sticky cache, filled by handleAssetsChange and
the load prefetch alongside writes
- replace the write-only overlay loop with one overlayLineage(map,
access) helper invoked for both 'w' and 'r' (net DRY)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: detect S3 assets passed as SDK object arg in ts parser
Mirrors merged PR #9181 so feat/asset-graph-view is self-contained
(local origin/main is stale and lacks it). Object/{ s3, storage }
form of writeS3File/loadS3File is now detected, not only the bare
s3:// string literal.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: regenerate wasm Cargo.lock + frontend package-lock
Lockfile churn from local wasm-pack (asset target) + npm operations
during the asset-graph work. No source/dependency-intent change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: revert to bezier graph edges; add parsing-assets hint
The sugiyama-waypoint routing looked worse than the original; revert
AssetGraphEdge/assetGraphLayout to the pre-routing bezier logic (same
as the flow editor's BaseEdge) and drop the now-unused route plumbing
from the canvas. Add a small 'Parsing assets…' hint shown while the
load-time prefetch sweep is still inferring folder scripts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor: extract pure resolveGraph merge + unit tests
Move the ~230-line graphWithDraft precedence/merge (base < session-
inferred < draft-seeded < open-script-live, +read/write/annotation
overlays, +dedup) out of the 1648-line route into a pure, testable
resolveGraph() module; the route's graphWithDraft is now a thin
$derived. Behaviour extracted verbatim. 10 unit tests cover the
precedence matrix. Phase 1 of the state/render split.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style: graph controls top-right, lift minimap, hide Save when unchanged
Controls -> top-right horizontal, no lock toggle; MiniMap !mb-10 so
it clears the activity bar; hide the per-script Save button when the
script is already at its latest save point (drafts still show Create).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: scope runtime-asset prune by id to spare static lineage rows
prune_runtime_assets deleted by (workspace_id, path, kind) tuple, so
trimming surplus usage_kind='job' rows for an s3 path also wiped the
static usage_kind='script'/'flow' producer rows for the same path —
silently breaking the asset-trigger cascade (fetch_producer_writes
found no writes; downstream never dispatched; required band-aid
re-syncs). Delete the surplus job rows by id instead; the inner query
is already scoped to usage_kind='job'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: don't re-pulse already-running jobs after they finish
The catch-up pulse re-added a completed job to the active set if its
start was within the (lagging) lookback window — even one we'd already
animated the whole time it ran — keeping its edges lit ~a poll
interval past completion (~5s after a 3.5s test). Track job ids seen
in-flight and skip the pulse for them; it still fires for hops whose
whole lifetime fell between two polls. Bound the set in lockstep with
eventsById; cleared on dispose.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: don't catch-up-pulse the runnable launched from the graph
If the poll never sampled a launched run's in-flight window, the
catch-up pulse re-flashed its edges one tick after it correctly
stopped (the page already animated it zero-latency via activeRunnable).
arm(launchedId) records the launched runnable id; catch-up skips it.
Cascade hops (other ids) still pulse. launchedIds cleared on stop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style: nudge graph controls left to clear panel toggle
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: partition value resolver + asset-cascade propagation
windmill-common/partition: pure resolver — time kinds (tz/format/start
anchor) + dynamic $.a.b JSONPath; 9 unit tests. asset_dispatch:
read the producer's resolved partition and thread it into every
cascaded subscriber's args + trigger.partition, so a chain resolves
once at the top. No migration (cascade needs no spec lookup). Stage
1+3 of pipeline partition runtime; run-start resolution is Stage 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: show args form in compact pipeline preview when script has inputs
AssetGraphDetailsPane keeps the compact (hideArgs) preview but, via a new
previewPanel.argsAboveLogs flag, renders a compact SchemaForm between the
floating Test button and the logs/result panel when the script declares
inputs (e.g. a partitioned script needing a `partition` arg). The preview
pane also grows ~18pts so the args form doesn't shrink logs/result.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: parser join-mode (`// trigger all`) + script_trigger.join_all
Stage A: JoinMode{Any(default),All} + `// trigger any|all` directive in
parse_pipeline_annotations; TriggerSpec::is_partition_bearing() (path
contains {partition}); join_mode threaded through all 4 asset-parser
crates (ts/py/sql/yaml). Stage B: reversible migration adds
script_trigger.join_all; insert_script_trigger writes it; deploy path
sets it from the parsed annotation. No reader yet (AND-join dispatch is
the next stage) so runtime behaviour is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: resolve pipeline partition at job execution time
Stage C: in handle_code_execution_job, once the script content is loaded,
parse the // partitioned annotation (free here) and resolve the concrete
partition once — schedule fire-time (scheduled_for anchor, not wall-clock)
for time kinds, triggering payload for dynamic. The value is injected
into the in-memory args the body sees (via a shadowed job clone) and
persisted back to v2_job.args so dispatch_asset_triggers propagates the
same value down the cascade. Already-set (explicit/backfill/cascade)
partitions are never re-resolved (run identity immutable); unresolvable
partitioned runs fail with a clear error. Integration test exercises the
full worker loop + cascade propagation.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: AND-join barrier for partitioned pipeline subscribers
Stage D: a // trigger all subscriber no longer fires on any input. New
join_pending_inputs slot table keyed (workspace, subscriber, partition);
fetch_subscribers now returns join_all and the dispatch loop records each
partition-bearing input arrival, pushing the subscriber once only when
every partition-bearing input it declares is present for that partition.
Per-partition slots, cleared on fire (re-accumulate, no double-fire),
skew-immune (unlike debounce). Case-3 guard: an unpartitioned producer or
a reference (non-{partition}) input never fires a partitioned join.
Integration test covers wait/fire/isolation/no-double-fire.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: opt-in // debounce for asset-cascade subscribers (parser + schema)
Stage E1+E2. Parser: script-level // debounce <dur> + per-// on
debounce=<dur> override (edge wins, else script default, else none =
fan-out, unchanged); TriggerSpec::Asset carries the per-edge override;
split_trailing_kv_opts separates the ref from trailing key=val opts.
Schema/deploy: reversible migration adds script_trigger.debounce_s;
parse_duration_secs (bare int or <n>s|m|h|d, fail-safe on garbage)
resolves the effective per-edge window at deploy and writes it per row.
No reader yet (dispatch wiring is E3) so runtime is unchanged. New unit
tests for the parser directive and duration parsing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: apply opt-in debounce to asset-cascade subscriber dispatch
Stage E3. fetch_subscribers now also returns debounce_s; push_subscriber
builds real DebouncingSettings (delay + a (subscriber, partition) key,
so distinct partitions never collapse and latest-in-window falls out)
instead of ::default() when the edge opted in. Default stays no-debounce
(fan-out — the prior deliberate behaviour, now overridable rather than
reversed). Wiring test asserts the dispatched job carries the configured
window/key and an undebounced edge carries none.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix: atomic AND-join gate + preserve resolved partition; drop scratch artifacts
Addresses local-review findings before PR:
- P1: record_and_check_join_slot was a non-atomic check-then-act on a
pooled connection; concurrent completion of a subscriber's last two
partition-bearing inputs on different workers could double-dispatch.
Now one transaction guarded by a tx-scoped advisory lock keyed on
(workspace, subscriber, partition) so the gate fires exactly once.
- P2: the preprocessed-args overwrite in result_processor replaced args
wholesale, dropping a partition resolved by resolve_partition_for_job;
the UPDATE now preserves an existing persisted partition key.
- P2: gate resolve_partition_for_job on a cheap code.contains check so
non-pipeline script jobs skip the annotation scan on the hot path.
- P2: remove 40 scratch screenshot PNGs, a flicker-debug script and a
local scheduler lock accidentally committed; gitignore the lock.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: AND-join fires once under concurrent upstream completion
Regression for the check-then-act race fixed by the advisory-locked
transactional gate: releases N producer dispatches simultaneously via a
barrier and asserts the AND subscriber is pushed exactly once and the
slot is cleared. The invariant holds for the correct gate regardless of
interleaving; a non-atomic regression fails it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: fuller partitioned join + multi-hop pipeline coverage
Exercises a complex pipeline combining options end to end: two
partitioned producers fanning into a // trigger all join, then a
multi-hop downstream chain. Asserts the resolved partition propagates
unchanged at every hop, chain depth increments per hop, the AND barrier
fires exactly once, and a second partition opens an independent slot
with no cross-partition bleed across the whole graph.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor: simplify pipeline code per review (dedup, single-parse, constant)
- ParseAssetsOutput::new() collapses the 6-line annotation copy-paste
across the 4 asset-parser crates to one call site.
- asset_dispatch: parse the cascade trigger object once and pass it to
the depth/partition readers instead of deserializing it twice; add a
TRIGGER_ARG constant for the previously stringly-typed key (3 sites).
- scripts deploy: drop a redundant debounce_default clone.
No behavior change; 29 parser + 6 dispatch integration tests green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: reap abandoned AND-join slots after a TTL (default 60d, per-slot)
join_pending_inputs slots are normally cleared when the join fires;
partial slots whose inputs never all arrive (upstream removed/renamed,
one-off dynamic partition key, permanent skew) would otherwise leak.
windmill_queue::asset_dispatch::reap_stale_join_slots, called from the
monitor's delete_expired_items loop, deletes a (workspace, subscriber,
partition) slot only when its MOST RECENT row is older than
JOIN_SLOT_TTL_SECS (60d) — per-slot, never per-row, so a legitimately
slow join is not corrupted mid-accumulation. Conservative default;
per-join configurable TTL via the annotation is a planned follow-up.
Test covers stale-reaped / fresh-kept / mixed-slot-kept.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* update
* feat: path-less native trigger markers + missing-trigger placeholder
* feat: pipeline // tag and // retry annotations + dispatch_event log
* fix: derive test-pane min from split-axis dimension (height in bottom layout)
* feat: show last run logs/result when a script node is selected
* fix: backfill asset rows from script.assets for pre-feature scripts
* feat: job-id link + dispatch popover above script log/result
* style: drop 'dispatched' label, keep just the check icon
* fix: drop tag picker from pipeline script editor (set via // tag annotation)
* Nicer UI
* refactor: move google ai proxy handling to windmill-ai (#9260)
* refactor: add ai proxy execution mode
* refactor: move google ai proxy handling
* refactor: share google ai request building
* fix: early return should consider failure_module result (#9241)
* fix(flows): flag noLogs jobs and lazily resolve them in log panel (#9099)
* fix(flows): flag noLogs jobs and lazily resolve them in log panel
* fix appending to flag
* fix: preserve WM_LOGS_SKIPPED sentinel on SSE/replay completion
pickMoreCompleteLogs resolved both sentinel and undefined to '', so the
SSE completion event (whose job field is fetched .without_logs()) would
clobber the sentinel placed by flagSkippedLogs. The module log panel
then saw '' instead of the sentinel, defeating the lazy-resolve path.
Also wire onLogsResolved on the OutputPickerInner inline LogViewer so a
lazy resolve writes back to flowStateStore.previewLogs, matching
ModulePreviewResultViewer and avoiding repeated fetches on remount.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(main): release 1.705.0 (#9229)
* chore(main): release 1.705.0
* Apply automatic changes
---------
Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
* chore: add playwright mcp for frontend verification (#9269)
* feat: CLI datatable serve / psql (#9267)
* feat(cli): add datatable list and run commands
* feat(cli): render datatable query results as a table
* feat(cli): serve datatables as a postgres-wire endpoint
* feat(cli): add 'datatable psql' to launch psql against the proxy
* feat(cli): route datatable serve by client-supplied database name
* override database list + password option
* fix: support extended queries in datatable serve
* fix: correct cloud size threshold log and parse CLI descriptions with parens/trailing comma
* refactor: extract raw_output envelope encoding into pg_raw_output module
---------
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
* oom_adj nit
* feat: add UV_PYTHON_INSTALL_MIRROR env and instance setting (#9271)
* feat: add UV_PYTHON_INSTALL_MIRROR env and instance setting
Allows operators to point `uv python install` at a private mirror of the
python-build-standalone releases. Configurable via the
`UV_PYTHON_INSTALL_MIRROR` env var or the `uv_python_install_mirror`
instance setting, with the env var as the boot fallback and the instance
setting taking precedence at reload.
Fixes WIN-1966
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: hoist uv_python_install_mirror binding above sandboxing branch
The non-sandboxed uv pip install branch referenced a binding that was
only declared inside the sandboxed branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: neutral placeholder for uv_python_install_mirror
The previous placeholder was the default public URL the setting is meant
to redirect away from. A neutral example mirror URL is clearer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(indexer): tell admins when ingress routes search to wrong pod (#9274)
* [ee] fix(indexer): tell admins when ingress routes search to wrong pod
When the IndexReader is absent on the pod handling a search request but
another pod is actively holding the indexer lock, the EE handler now
returns a tailored error pointing at the ingress/load-balancer
configuration instead of the generic "indexer not running" message.
The indexer status endpoint reads the DB lock so it reports "running"
from any pod, but search endpoints need the in-memory IndexReader that
only exists on the lock holder. In multi-replica deployments this looks
like the indexer is healthy but every search 404s.
Companion: windmill-labs/windmill-ee-private#TBD
Fixes WIN-1968.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to eb18d7b4c0e37fea3f6e1e2cc44e0fddd74ff817
This commit updates the EE repository reference after PR #586 was merged in windmill-ee-private.
Previous ee-repo-ref: 7dd43d1850813071cc18ba49ba090583e7321f4b
New ee-repo-ref: eb18d7b4c0e37fea3f6e1e2cc44e0fddd74ff817
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat(cli): add `wmill init prompts` and custom override slot (#9266)
* feat(cli): add `wmill init prompts` and custom override slot
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(cli): replace init prompts with refresh prompts + AGENTS.md/AGENTS.cli.md split
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cli): dedupe claude skills via @-includes and add prompts freshness check
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(cli): drop migration-choice flags from `refresh prompts`
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(cli): add 'Running and previewing local changes' section to AGENTS.cli.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): write full skill content to .claude/, drop @-include wrapper
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cli): reconcile CLAUDE.md the same way as AGENTS.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): address PR review nits — argv parsing, lazy import, comment detection, error propagation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: add yolo mode for ai chat tools (#9258)
* feat: add yolo mode for ai chat tools
* nit
* fix: align chat footer controls
* feat: add ai chat autonomy modes
* feat: add autonomy mode dropdown
* fix: highlight yolo autonomy icon
* fix: auto accept flow edits
* fix: hide unsupported autonomy modes
* fix: handle auto-accept flow editor races
* fix(debugger): add non-root user support to Dockerfile (#9277)
Mirrors the main Windmill Dockerfile pattern: creates a windmill user
(UID/GID 1000) and makes cache/work directories world-writable so the
image runs cleanly under Kubernetes securityContext.runAsNonRoot or
runAsUser: 1000 without permission errors on Bun, pip, or windmill
cache writes.
Fixes WIN-1969
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): enforce RLS and scope check on user-supplied X-Resource-Path (#9276)
* fix(ai): enforce RLS and scope check on user-supplied X-Resource-Path
The AI proxy handler accepts an X-Resource-Path header to override the
configured workspace AI provider. When supplied, the handler loaded the
resource value from the resource table using the root DB pool with no
resources:read scope check, so any authenticated workspace user could
point X-Resource-Path at a restricted AI resource (e.g. one in a folder
they cannot read) and the proxy would use that resource's provider
credentials for the outbound AI request.
For user-supplied resource paths, now require resources:read:{path}
scope and fetch the resource through user_db.begin(&authed) so RLS
enforces the same folder/group boundary as the resource API. The RLS-
scoped $var: resolution stays in place as defense in depth. The
admin-configured workspace/instance ai_config path is unchanged.
Fixes WIN-1971
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(ai): regression test for X-Resource-Path RLS enforcement
Cover all four cases:
- non-admin pointing X-Resource-Path at a restricted resource is rejected
- non-admin pointing it at a resource they own still works
- admin can point it at any resource
- workspace-configured proxy flow (no X-Resource-Path) is unchanged
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: add userdraft listing primitives (#9268)
* feat: add userdraft listing primitives
* fix: cancel stale userdraft discard writes
* docs: remove global ai userdraft plan
* feat(nsjail): optional disk-backed /tmp via instance setting (#9272)
* feat(nsjail): optional disk-backed /tmp via instance setting
* test(nsjail): unit-test tmp mount resolver and narrow visibility
* refactor(nsjail): switch tmp backing to select + conditional UI
* ui(nsjail): make tmpfs the visible default in /tmp backing select
* fix(nsjail): refuse preexisting jail_tmp to block symlink escape
* fix(nsjail): allow jail_tmp reuse on sequential nsjail calls
Codex flagged that python/ruby/rust executors invoke nsjail twice per
job_dir (install then run). The previous resolver treated any preexisting
jail_tmp as hostile and silently fell back to tmpfs on the second call,
so disk-backed mode never reached the main script run for those langs.
Use symlink_metadata().is_dir() to distinguish a real directory left by
an earlier call in the same job_dir (safe to reuse) from a symlink or
other entity (still refused, as the codebase-tar escape requires).
Also loosen the frontend visibility predicate: only hide nsjail settings
when job_isolation is explicitly 'none' or 'unshare', so deployments
that enable nsjail via DISABLE_NSJAIL=false with no DB setting can
still see the controls.
* chore(main): release 1.706.0 (#9270)
* chore(main): release 1.706.0
* Apply automatic changes
---------
Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
* fix(nsjail): gate unix-symlink test behind cfg(unix) for Windows build (#9280)
The disk_backed_refuses_preexisting_symlink_at_jail_tmp test calls
std::os::unix::fs::symlink directly, which doesn't exist on Windows
targets. Without a cfg gate, `cargo check --tests` fails on Windows
with E0433. Other symlink call sites in this crate (php_executor,
bun_executor, rust_executor, etc.) already follow this pattern.
Fixes WIN-1972
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Reduce slim image vulnerability surface (#9279)
* Reduce slim image vulnerability surface
* chore(docker): drop apt-get upgrade -y from slim images
apt-get upgrade hurts build reproducibility (same Dockerfile + same
commit at different times produces divergent images) and trips hadolint
DL3005. The freshness it buys is dominated by simply rebuilding against
the periodically-refreshed debian:bookworm-slim base image.
The --no-install-recommends and apt-list cleanup wins are kept.
---------
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
* fix(git-sync): bump to hub/28234 with stateless gpg.program wrapper (WIN-1974) (#9282)
* fix(git-sync): revert LATEST_GIT_SYNC_SCRIPT_PATH to hub/28230 to restore GPG-signed deploys (WIN-1974)
hub/28231 (PR #9230) is the "thin" script that hands the actual `git commit`
to the CLI's hidden `sync git-deploy`. The hub script still does the GPG
setup (import key into a fresh GNUPGHOME, dummy `gpg -bsau` to warm the
agent passphrase cache, then `git config user.signingkey` + `commit.gpgsign`
locally), but the commit no longer runs in the same `git_push` flow — it
runs minutes later inside the CLI after workspace API resolution, zip pull,
file extraction, and lockfile autofill. By the time the spawned `git commit`
asks gpg-agent for the cached passphrase, the cache state is no longer
reliable (or the spawned `gpg` ends up talking to a fresh agent), so signing
fails non-interactively with `gpg failed to sign the data`.
hub/28230 is hub/28217's in-script logic rebuilt with windmill-cli@1.703.3:
the GPG setup and the in-script `sh_run("git commit ...")` happen back-to-back
in `git_push`, so the cache is always fresh. It preserves wm_deploy / fork
branch behavior, the EE deployment-callback `main()` signature is unchanged,
and the only min-version check in EE (`is_script_meets_min_version(28103)`)
is comfortably below 28230 — so this revert is safe.
Forward fix (separate PR): publish a new thin script that, alongside the
existing GPG setup, writes a `gpg.program` wrapper using `--pinentry-mode
loopback --passphrase-file` so signing is independent of the agent's cache
state. Re-bump past 28231 then.
Fixes WIN-1974
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(git-sync): check in source-of-truth for the next hub script (gpg.program wrapper)
This is the script that will be published to hub.windmill.dev once verified
on a customer GPG-signed deploy. It replaces hub/28231's agent-cache
pre-warm (`gpg -bsau` with --passphrase) with a stateless gpg.program
wrapper + chmod-600 passphrase file. Every git-invoked gpg call goes
through the wrapper, which always uses --pinentry-mode loopback (and
--passphrase-file when a passphrase exists). Signing no longer depends on
gpg-agent having a cached passphrase by the time the CLI's `git commit`
runs — which closes WIN-1974.
Not wired in yet: LATEST_GIT_SYNC_SCRIPT_PATH stays on hub/28230 until this
script is uploaded and the new hub id is known. This file is checked in so
the diff is reviewable, future bumps have a source of truth, and a CLI
regression test can `cat` it for fixture parity.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(frontend): skip format/pattern validation for $var/$res/$jsonvar references in ArgInput
A resource field with a `pattern` constraint (e.g. the gpg_key.private_key
field, whose pattern enforces a `-----BEGIN PGP PRIVATE KEY BLOCK-----`
prefix) rejects values like `$var:u/me/gpg-private-key` with an "invalid
format" error in the resource editor — even though `$var:`/`$res:`/`$jsonvar:`
are placeholders the backend resolves at runtime, not the actual string
that needs to match the regex.
Bail out of all format/pattern checks (email, ipv4, ipv6, uuid, custom
pattern) when the value is one of these references. Required/numeric
bounds/array checks still apply since they're shape-level, not regex.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(git-sync): bump LATEST_GIT_SYNC_SCRIPT_PATH to hub/28234 (gpg.program-wrapper fix)
hub/28234 is the forward fix for WIN-1974: replaces hub/28231's agent-cache
pre-warm (which became stale by the time the CLI's `git commit` ran) with
a stateless `gpg.program` wrapper that uses `--pinentry-mode loopback`
(and `--passphrase-file` when a passphrase exists) on every gpg invocation.
Bundled CLI is windmill-cli@1.705.0.
Verified via reproducer at /tmp/git-sync-diff/test-gpg-fix.sh: deliberately
killing gpg-agent between GPG setup and `git commit` reproduces the
customer's `gpg failed to sign the data` error verbatim under the old
flow, and the wrapper signs through it. Holds for passphrase-protected
keys, split-subkey [C]+[S] layouts, and unprotected keys.
Drops the local source-of-truth copy (`hub-scripts/`) — hub is canonical
now that 28234 is published.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(git-sync): drop verbose comment above LATEST_GIT_SYNC_SCRIPT_PATH
The git history (this PR) carries the why; the constant name + value carry
the what.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(cli): wmill sync git-deploy stops committing; caller owns commit+push (#9284)
Single contract for the deployment-callback path: the CLI does branch
checkout + pull, the caller (hub script in production, test in test)
does git add + commit + push. This restores the WIN-1974 invariant —
GPG setup and `git commit` run back-to-back in the same process, so
the agent's pre-warmed passphrase cache is still warm at sign time —
without needing a `--skip-commit` flag for the hub case and a default
"also-commit" for everything else. Same behavior in every call site.
Changes:
- sync.ts: drop the gitSyncDeployPush call from pull()'s deploy path
(both the onlyCreateBranch fast-return and the post-pull commit).
`gitSyncDeployPush` stays exported for any caller that wants the
same commit/push semantics — just not invoked by the CLI subcommand.
- gitsync_promotion.test.ts: e2e test now does its own git add +
commit + push after `wmill sync git-deploy`, mirroring what the
hub script does in production. Same regression coverage
(wm_deploy branch created in Case A, main untouched; main updated
in Case B, no new wm_deploy).
CLI typecheck unchanged (two pre-existing TarAsZip errors at lines
2578/3307, present before this PR). All 743 unit tests still pass.
The accompanying hub script (option-C — CLI for branch+pull, script
for commit+push) lives at /tmp/git-sync-diff/sync-script-to-git-repo-windmill.option-C.ts.
Once published, a follow-up bumps LATEST_GIT_SYNC_SCRIPT_PATH to its id.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* bump git sync to 28236
* fix: fork compare visibility for non-admins and stale-token superadmins (#9283)
* fix: use fork-scoped authed for fork visibility in compare_workspaces
* test: add EE end-to-end repro for fork rename visibility
* chore: restore concurrency_locks sqlx cache lost in cleanup
* test: add regression for stale-superadmin-token fork visibility bug
* chore: update sqlx cache for new test queries
* chore(main): release 1.706.1 (#9281)
* chore(main): release 1.706.1
* Apply automatic changes
---------
Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
* feat: add wmill job rerun subcommand (#9275)
* feat: add wmill job rerun subcommand
* feat: add wmill job restart subcommand for flow restart-at-step
* chore(system_prompts): point plugin skills sync at plugins/windmill/ (#9287)
* chore(system_prompts): point plugin skills sync at plugins/windmill/
The plugin checkout's plugin folder is being renamed from
`plugins/windmill-code-plugin/` to `plugins/windmill/` to shorten the
slash-command namespace and align with the matching Cursor plugin
layout.
Paired with windmill-labs/windmill-claude-plugin#8. That PR must merge
first so the next sync run finds the new folder.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(system_prompts): update plugin-dir example to plugins/windmill
Co-authored-by: centdix <centdix@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: centdix <centdix@users.noreply.github.com>
* fix(cli): wmill sync pull updates wmill-lock.yaml for raw apps (#9289)
* fix: flow recording teardown crash + rename package to @windmill-labs/components (#9288)
* fix: guard against null recording during FlowRecordingReplay teardown
Navigating away from a flow recording inside a workspace file-tree view
threw `TypeError: Cannot read properties of null (reading 'flow')` from
FlowGraphViewer once during the teardown tick.
Svelte 5 compiles child component props as live getters that close over
`$$props.recording.flow`. When `recording` flips to null on the parent's
navigation, an outer `{#if !recording?.flow}` doesn't stop those getters
from firing one more time as derived effects re-evaluate before the
unmount lands — so the getter dereferences null and throws.
Fix at the two layers where the deref actually happens:
- FlowRecordingReplay: use `recording?.flow` at the binding sites
(FlowViewer + graph-snippet FlowGraphViewer) so the compiler emits an
optional-chained getter, and guard the snippet branch with
`{:else if recording?.flow}` so it doesn't mount when there's nothing
to show.
- FlowGraphViewer: finish the optional chaining the rest of the file
already used everywhere else (`flow?.value?.skip_expr`,
`flow?.value?.cache_ttl`, `flow?.schema`). When the upstream
binding returns undefined during teardown, the graph degrades to an
empty frame instead of crashing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: rename package to @windmill-labs/components
- frontend/package.json: rename `windmill-components` → `@windmill-labs/components`
- frontend/publish.sh: drop the in-place sed rename dance; the checked-in name now matches what's published, so `npm run package && npm publish` is enough
- frontend/package-lock.json, system_prompts/auto-generated/prompts.d.ts: regenerated by `npm run package` under the new name
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* default script name
* save logic
* Keyboard nav
* finish keynav
* nits
* CI fix
* nit stop propagation
* Merge branch 'main' into feat/asset-graph-view
* commit
* update
* fix: cropped save button on small screens
* progress
* managed scheduled removed
* all
* progress
* feat: add data upload pipeline trigger with auto S3 picker
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: avoid pane editor remount flicker when deploying a pipeline draft
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: show only the edited script's I/O in the asset graph, not the saved version's
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: derive script asset rows server-side at deploy
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: shared fixture corpus keeps annotation parsers in parity
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: dev-run draft pipeline chains, live badges, deploy drift warning
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor: ungate cascade producers, squash pipeline migrations
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: drop committed cli-sync fixtures and stray screenshots
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: show skip-asset-dispatch flag as badge instead of args row
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: pipeline view mode default with activity feed, drafts overlay chip
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: treat DROP TABLE as table-level write in sql asset parser
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: wmill datatable create + actionable sql extension error
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: ephemeral data-pipelines demo sync repo zip for handoff
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: wmill pipeline list/show renders the asset DAG in the terminal
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* nits
* nits
* nits
* nits
* fix: defer draft persist-back past the batch so discard sticks first click
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: band-reserving tidy-tree asset graph layout with join breakpoints
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: route skip-layer and long graph edges around occupied columns
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: seed s3 template outputs with canonical leading-slash paths
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* all
* feat: bundle data-pipeline drafts into the DB-backed user draft system
Pipeline drafts were browser-only (localStorage `pipeline-<folder>`), so they
didn't sync across devices, weren't server-visible, and never showed in the
drafts list. Store them instead as one per-user `draft` row of a new
`data_pipeline` kind, keyed at the folder (`f/<folder>/data_pipeline`), holding
the same `{ drafts, activeDraftPath }` bundle.
Stage 1 — backend kind: add `data_pipeline` to DRAFT_KIND (migration) and
`UserDraftItemKind` (deployed_table=None, private). The list/update handlers
and folder-path access check already cover a backing-table-less kind.
Stage 2 — sync: add `GET /drafts/get_own/{kind}/{path}` so an editor with no
deployed-overlay GET can load its own draft. The pipeline page now hydrates
from the DB on mount (one-time localStorage import for in-flight drafts) and
persists via UserDraftDbSyncer (debounce + optimistic-concurrency), keeping a
localStorage crash mirror.
Stage 3 — surface: the drafts review page renders the bundle as a "pipeline"
row that opens `/pipeline/<folder>` (open-only; excluded from bulk deploy).
Verified end-to-end in-browser: DB-seeded draft hydrates to "Edit (1)", edits
persist back, and the row shows with Open pipeline / Discard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: pipeline Activity panel grouping, run↔graph highlight, deploy-conflict handling
Activity panel (view mode):
- Group cascade runs by the connected component of the asset-dispatch graph
(new GET /jobs/asset_dispatch_edges over the dispatch_event table, incl.
join_pending inputs), headed by the earliest originating run + its trigger,
with a "+N" chip for joins fed by multiple triggers.
- Success/failure count histogram with drag-to-filter brushing, an always-on
time axis + per-bar tooltips, a Reset, and Last hour/24h/48h/7/30/90d ranges.
- Node run-count/status badges now derive from the same merged historic+live
events the panel shows (previously session-only).
Run ↔ graph highlight:
- Hovering a run row (or a group header → the whole cascade) rings the
node(s), animates their incident edges, and borders the adjacent assets in
the edge hue (blue write / gray read); expanding a run pins a soft-blue ring.
- Switching edit→view re-surfaces the Activity feed.
Deploy:
- Live-content autosave for the open pipeline draft + an autosave indicator.
- Re-saving a script now chains off the hash just created instead of a stale
parent_hash (fixes the "lineage must be linear" error on a second save), and
a genuine concurrent deploy opens a keep-mine / view-latest conflict modal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: pipeline editor badge requires asset-parse, not just main-function parse
A pipeline script's asset lineage is load-bearing — a deploy that can't parse
assets silently records no edges. The editor "parsable" dot only reflected
inferArgs (the main function), so a body the asset parser rejects (e.g. a
trailing `/////` in DuckDB) still showed green and deployed with empty lineage.
ScriptEditor gains `requireValidAssets` (set by the pipeline pane); when on, the
EditorBar badge is green only if BOTH the main function and inferAssets parse,
with the tooltip distinguishing "Main function not parsable" / "Assets not
parsable" / "Parsable".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: route asset-graph edges around nodes that sit in their path
Edges could draw straight through an unrelated node (a join fan-out or long
cross-component edge), making it ambiguous whether that node shared the input.
AssetGraphEdge only saw its own endpoints, so it could only detour the
near-vertical same-column skip case.
The canvas now (once per layout, O(edges × nodes) — no per-frame cost) samples
each edge's straight run against every non-incident node center and, on a
crossing, passes a clear gutter lane to the edge via `data.detourX`;
AssetGraphEdge routes the rounded-orthogonal detour through it. Verified: 0
edge↔node box crossings on the orders pipeline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: deploy pipeline drafts with freshly-inferred assets, not a stale snapshot
"Save all" spread `...draft.script` into createScript, which carries a `assets`
snapshot that isn't refreshed when the body is edited. So a renamed/removed
output (e.g. an old `CREATE TABLE exciting_en32z9` later changed to
`exciting_880909`) was re-deployed as a phantom write edge and lingered as an
orphan asset on the graph — shown with no producer, and shifting position on
click as the graph re-derived.
saveDraft now re-runs inferAssets on the current body and passes the result as
`assets`, overriding the snapshot — mirroring the per-pane save. The backend
clears+reinserts from the sent set, so a re-deploy drops the stale rows.
Verified: deploying with the fresh asset set removes the orphan from the graph.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: collect upstream reads from CTAS and CREATE VIEW in SQL asset parser
`CREATE TABLE x AS SELECT … FROM y` (and `CREATE VIEW`) recorded only the
write to x — the source read of y was silently dropped. Table-level reads are
gathered in the `Statement::Query` arm via handle_table_with_joins; the generic
table-factor visitor only picks up read-functions and string literals, not
plain `FROM <table>` references. The AS-query of a CTAS isn't a
`Statement::Query`, so its FROM tables were never walked. On the pipeline
canvas this meant a `datatable://…` upstream consumed by a CTAS step showed no
read node/edge — the step looked like it produced its output from nothing.
Factor the Query arm's read collection into handle_query_reads and call it from
the CreateTable (when it has an AS-query) and CreateView arms, balancing the
cte_name_stack push in post_visit_statement. Updated the drop_then_create test
(which had pinned the old drop-the-read behavior) and added CTAS + CREATE VIEW
read coverage. Verified against the rebuilt asset wasm: the live editor now
infers the read.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* update
* updates
* refactor: dedup asset-graph code, squash migrations, drop artifacts
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf: gate asset dispatch on a cached per-workspace producer set
Cache the producer-path→writes map per workspace and invalidate it from the asset-clear paths via the notify_event polling system, so a top-level script/preview completion that isn't an asset producer costs an in-memory lookup instead of a per-completion query.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: remove dead unquote fn that failed backend check under -D warnings
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: green the frontend check (pin published wasm-asset, fix type errors)
Pin windmill-parser-wasm-asset to the published 1.728.1 (was a file: link to a gitignored, CI-unbuilt pkg-asset). Exclude test files from svelte-check (the parity test reads a backend fixture via node:fs, which the browser app tsconfig has no @types/node for; vitest still runs them). Fix pre-existing branch type errors: drop the unsupported 2nd getScriptByPath arg, cast script.schema to Schema for inferArgs, coerce has_preprocessor to a definite boolean, and wrap the cancelJob handler so it isn't possibly-undefined.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: move pipeline partition resolution to ee-private (free-CE)
Partition resolution becomes a private module (partition_ee in windmill-ee-private, hidden from the public repo) with an OSS no-op fallback (partition_oss); call sites resolve via the aliased windmill_common::partition. Not enterprise-gated — free to run in CE. Bumps ee-repo-ref to the ee branch carrying partition_ee. Verified building in default, private, and private,enterprise (offline). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: move asset-cascade join/debounce/retry to ee-private (free-CE)
Join barrier, debounce, and retry become the private windmill_queue::cascade module (cascade_ee in windmill-ee-private); OSS gets cascade_oss no-op fallbacks (plain OR fan-out). Core cascade stays public. Bumps ee-repo-ref. Verified default/private/private,enterprise. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: skeleton enterprise pipeline freshness + backfill (TODO, ee-private)
Gated windmill_common::pipeline_advanced (private; pipeline_advanced_ee) with OSS fallback; entry points return a clear not-implemented error. Deploy surfaces a TODO when a script declares // freshness. Bumps ee-repo-ref. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: repair asset_trigger_dispatch test after cascade carve-out + cache its queries
Stage-2 moved reap_stale_join_slots to windmill_queue::cascade; update the integration test's import. Also commit the test's sqlx query cache (was never prepared with --tests, so SQLX_OFFLINE cargo test failed pre-existing). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: invalidate producer-cache in asset dispatch tests (mirror deploy)
The tests seed asset rows directly and run no notify poller, so the per-workspace producer cache went stale across tests → 0 dispatched. Clear it at the seed point, as a deploy would via notify_event. All 8 asset_trigger_dispatch tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to ba677ea142011462ad4dfe77e8375a6dd274cdef
This commit updates the EE repository reference after PR #619 was merged in windmill-ee-private.
Previous ee-repo-ref: 925c350cff55d3ea738d9e2e4098d9ce4bdda418
New ee-repo-ref: ba677ea142011462ad4dfe77e8375a6dd274cdef
Automated by sync-ee-ref workflow.
* test: disable producer cache in asset dispatch tests (isolated-DB safe)
The .remove(WS) approach still raced: #[sqlx::test] gives each test its own DB but they share one workspace id, so the WS-keyed process-global cache clobbered across DBs under concurrent threads. Add an ASSET_PRODUCER_CACHE_DISABLED test hook and set it in the tests so every dispatch reads its own DB. 8/8 pass at --test-threads=10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: replace asset-cascade depth cap with cycle detection
The hardcoded MAX_CHAIN_DEPTH=5 truncated legitimate deep pipelines (silently — the check returned before event logging). Replace it with per-edge cycle detection: carry the producer lineage in trigger.chain and skip only a subscriber already in the chain, recording a visible cycle_detected dispatch_event. Acyclic pipelines of any depth now cascade fully; a high MAX_CHAIN_LEN backstop guards against runaway. Tests + UI label updated; 8/8 pass at --test-threads=10.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: update dispatch_event reason examples (depth_cap → cycle_detected)
Comment-only; the migration is idempotent and already in the potentially_stale self-heal list, so the checksum change re-applies cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: park cascade retry (P1 dead-end) + clear stale script_triggers on rename
Two deploy-path fixes:
- Retry is parked: a retried subscriber is wrapped in a SingleStepFlow, whose run is a flow step and ineligible for asset dispatch, so it would silently dead-end the cascade (P1). Stop persisting retry to script_trigger and warn at deploy; TODO(pipeline-retry) to re-enable once dispatch handles flow-wrapped producers. (Dispatch plumbing kept + still tested via direct seeding.)
- Rename leaves stale script_trigger rows: clear was keyed on ns.path only, so old-path '// on' edges lingered and could trigger a script later recreated at that path. Also clear the old path on rename (assets already handled via the parent-hash clear).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
Co-authored-by: hugocasa <hugo@casademont.ch>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Arnaud <31803803+Araden14@users.noreply.github.com>
Co-authored-by: Diego Imbert <diego@windmill.dev>
Co-authored-by: centdix <40307056+centdix@users.noreply.github.com>
Co-authored-by: Diego Imbert <70353967+diegoimbert@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Aldrin Jenson <aldrinjenson@gmail.com>
Co-authored-by: centdix <centdix@users.noreply.github.com>
NetSuite is a per-instance OAuth provider (account-specific authorize/token
URLs), registered via connect_config_template. Its authorize endpoint
requires scope=rest_webservices, so the template mechanism gains an
optional scopes field copied into the built connect_config.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(workspaces): add skip_email option to invite_user and add_user endpoints
The workspace invite_user and add_user API endpoints unconditionally sent
notification emails when SMTP was configured, with no way to suppress them
per-request. This is noise for automated workflows that programmatically add
users to workspaces.
Add an optional `skip_email: Option<bool>` field to `NewWorkspaceInvite` and
`NewWorkspaceUser`, following the existing pattern on `NewUser` used by
POST /api/users/create, and guard the `send_email_if_possible` calls with
`if !nu.skip_email.unwrap_or(false)`. The field is optional, so existing
clients are unaffected.
The auto-add code paths in workspaces_ee.rs (domain-based and instance-group
auto-add) are auto-triggered and take no API parameter, so they are left as-is.
Fixes WIN-2068
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(workspaces): make workspace invite/add emails toggleable via instance setting
Replace the per-request skip_email approach with an instance-level setting
`disable_workspace_invite_emails`. When enabled, the email notifications sent by
the workspace invite_user and add_user endpoints are suppressed. Useful for
instances where users are added programmatically (e.g. CI pipelines that fork
workspaces and add users) and the invite emails are noise.
Backend:
- Add `DISABLE_WORKSPACE_INVITE_EMAILS_SETTING` global setting constant.
- Guard the `send_email_if_possible` calls in invite_user and add_user with a
read of that setting (via the existing `load_value_from_global_settings`
helper). Defaults to false, so existing behavior is unchanged.
- Revert the per-request `skip_email` field on NewWorkspaceInvite /
NewWorkspaceUser and the corresponding openapi additions.
Frontend:
- Expose the setting as a boolean toggle in the SMTP tab of the instance
settings (superadmin).
The auto-add paths in workspaces_ee.rs are unaffected.
Fixes WIN-2068
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): gate disable_workspace_invite_emails toggle behind EE
Email delivery (send_email_if_possible) is a no-op outside the EE/private
build, so the toggle has no effect on a pure-OSS instance. Add `ee_only: ''`
to match the sibling SMTP settings: the toggle is grayed out (with an EE badge)
on non-EE instances instead of rendering as an active no-op control.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): don't EE-gate disable_workspace_invite_emails toggle
The earlier ee_only addition was based on the false premise that the
workspace invite/add emails are license-gated. They are not: SMTP
configuration (SmtpSettings) and email sending (send_email_if_possible)
have no enterpriseLicense check — they only require the closed-source
build with SMTP configured. The sibling smtp_settings carries ee_only: ''
but its smtp_connect field renders no SettingCard label, so that flag is
inert (no badge, no disable). On a plain boolean field ee_only is fully
active, which incorrectly grayed out the toggle and showed an EE badge.
Drop ee_only so the control matches the actual non-license-gated behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): summary-based conversation compaction
Replace drop-oldest compaction with summary-based partial compaction: when
a send would cross the context-window trigger, summarize the older prefix
into one message and keep the recent tail verbatim, replacing the prefix in
both the model context and the visible transcript with a collapsible
boundary. Drop-oldest remains a fallback; a circuit breaker disables the
summary round-trip after repeated failures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* nit
* fix(ai-chat): address review findings on summary compaction
- Stop during an in-flight summary no longer falls through to a destructive
drop-oldest compaction. The aborted controller short-circuits the fallback
and its save, so the cancel path rolls the unsent turn back cleanly instead
of permanently dropping older history (P1).
- Preserve the original chat title across compaction: once the summary
boundary leads the transcript, reuse the title computed before compaction
rather than re-deriving it from the first surviving tail message (P2).
- Strip every <analysis> block from the model's summary, not just the first,
so extra scratchpad blocks can't leak into context (P2).
- Reindent AIChatMessage.svelte / ContextUsageIndicator.svelte (prettier).
Adds regression tests for the abort path, title preservation, and
multi-analysis stripping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* nit
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: restrict agent-worker global setting reads to an allowlist
Add AGENT_WORKER_READABLE_SETTINGS allowlist of the operational settings
agent workers load over HTTP, with a helper used by the agent endpoint to
reject any other key. Bump ee-repo-ref for the companion EE handler change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 3fab9f01ecce3dad0aa9b9c544d41f1e88bc81dd
This commit updates the EE repository reference after PR #615 was merged in windmill-ee-private.
Previous ee-repo-ref: 8a657066fda1c5ffe225588bce6c349cffd81e98
New ee-repo-ref: 3fab9f01ecce3dad0aa9b9c544d41f1e88bc81dd
Automated by sync-ee-ref workflow.
* fix: make agent-worker setting gate a blocklist instead of allowlist
Switch is_setting_readable_by_agent_worker to deny-by-exception: serve every
global setting to agent workers except AGENT_WORKER_BLOCKED_SETTINGS (the
instance secrets). Update tests and bump ee-repo-ref for the companion comment
change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: remind to blocklist new secret settings for agent workers
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 9e4dadafb44ba953a7d2af2be12b92be98d86b66
This commit updates the EE repository reference after PR #618 was merged in windmill-ee-private.
Previous ee-repo-ref: 8e32afb69ffc4d5f080c0c4bc6b023d57d0f39ae
New ee-repo-ref: 9e4dadafb44ba953a7d2af2be12b92be98d86b66
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* oauth: map Coupa instance to instance_url resource arg
Coupa's managed client-credentials connect collects an instance name to
host-pin the token URL but had no resource_mapping, so the created resource's
instance_url (the API base URL the hub scripts build on) stayed empty. Add the
mapping, mirroring ServiceNow, so the entered instance fills it automatically.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* oauth: default Coupa client-credentials scopes (cc_scopes)
Prefill the connect dialog's scope field with the core.* scopes the Coupa hub
scripts exercise — read+write for suppliers/purchase_orders/requisitions/invoices,
read-only for contracts/expenses (the shipped scripts only read those). Scope
names verified against the Coupa scope docs and corroborated in production code.
The user can trim them to what their OIDC client is granted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(frontend): replace other-user draft "View JSON" with "View Diff"
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): replace other-user draft "Fork" with in-place "Load"
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(frontend): detect first overlay edit by value divergence, not a timer
Replaces the 700ms arming timer (which leaked across sessions and silently
swallowed sub-window edits) with a deterministic check: a blocked save opens
the overwrite prompt only once the cell value diverges from the loaded value.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): overlay leak on revisit, diff z-index, home-popover edit affordances
- Clear a stale "editing another user's draft" overlay when its editor is
reloaded without a fresh Load, so returning to the item edits our own draft.
- Open View Diff above the others-drafts modal (close it first) instead of
rendering the drawer behind it.
- Add an Edit button to our own row in the home draft popover; use a pencil
icon (not a download) for Load.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: admin "Migrate" action for legacy drafts (delete / assign to self)
Adds an admin-gated `POST /drafts/migrate_legacy/{kind}/{path}` endpoint to
resolve pre-migration workspace-level drafts (email NULL): delete the row, or
move its value onto the admin's own row. Surfaces a "Migrate" button on legacy
rows in the home-page draft popover and the in-editor others-drafts modal
(workspace admins / superadmins only), opening a modal with Delete and
Assign to self.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): close home draft popover before opening View Diff / Migrate
The hover popover sits above the diff drawer and migrate modal (z-index), so
it covered them. Close it first so they render on top.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): remount the flow builder on "Reset to draft" from an overlay
FlowBuilder captures the flow at mount, so reloading the value alone left the
foreign graph on screen — reset appeared to do nothing. Force a remount
(renderEditor=false → loadFlow) like navigation does. Scripts (imperative
setCode) and apps (redraw++) already remount, so only flows needed this.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): refresh the home row after migrating a legacy draft
invalidateAll() didn't refetch the home list (it loads items client-side), so
the legacy badge entry lingered after delete / assign-to-self. Bubble an
onMigrated callback up to the row's `change` event, reusing the same reload
chain (Item → ItemsList loadScripts/Flows/Apps) as delete/archive.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* nit
* nit
* fix(frontend): match app overlay baseline to the migrated value
AppEditor migrateApp()s the app on mount, so the draft cell settles to the
migrated value. The overlay used the raw loaded value as the divergence
baseline, so a post-mount mirror write could trip "Overwrite your current
draft?" before any edit. Migrate the baseline too (like the deployed-baseline
and raw_app bundle do) so it matches the settled cell.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): address review on legacy-draft migrate + overlay
- Legacy "Assign to self" now confirms before replacing an existing own draft
(MigrateLegacyDraftModal gains an `ownDraftExists` step, threaded from the
home badge and the in-editor others-drafts modal).
- Gate overlay mode on a per-response `hasOwnDraft` instead of the sticky
`loadedFromDraft`, so navigating to a no-own-draft item in the same editor
route can't wrongly enter overlay. Fixed in all 4 editor routes.
- Raw-app "View Diff" now projects the deployed app into the flat draft-bundle
shape (via a shared `extractDataConfig`) instead of diffing `.value` against
the bundle, so the drawer shows a real diff.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: add ducklake schema support to the database manager
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: support schema in wmill.ducklake("name:schema") template helper
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: preserve schema when parsing ducklake asset/favorite paths
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: regenerate system prompts for ducklake schema syntax doc
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): wire DB-backed autosave into the whitelabel flow SDK
FlowWrapper (the @windmill-labs/components flow editor entry) was never
updated after DB-backed user drafts moved autosave wiring to the page
layer, so the SDK editor had no autosave and never rendered the
AutosaveIndicator. Back the bound store with a per-user UserDraft handle
(workspace-guarded so it no-ops before a workspace exists) and pass
liveEditorDraftStoragePath so the indicator and Ctrl/Cmd+S flush engage.
Also set $workspaceStore on the /test_dev/sdk_flow harness page, which
lives outside the (logged) layout and so had an empty workspace store
(mirrors the sibling sdk_resource page).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): shared test_dev header to log in + set the SDK token
Add a common TestDevHeader (rendered by a test_dev/+layout) that logs in
(email/password → bearer token), lets a token be pasted/set manually,
picks the workspace, loads the user, and persists the session across
reloads — mirroring the React SDK's initializeClients. test_dev routes
live outside the (logged) layout, so this is the single place that wires
OpenAPI.TOKEN + workspaceStore + userStore for the SDK demo pages.
Drop the now-redundant per-page workspace/user wiring from sdk_flow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(frontend): reuse usePageDraftSync in the flow SDK instead of a parallel copy
FlowWrapper hand-rolled UserDraft.useMany + a manual seed effect, duplicating
the core of usePageDraftSync but dropping recordRemoteSync/seedBaseline/discardIf
— a divergence that would drift. The only reason it couldn't reuse the helper
was that useReactive passes workspace straight into useMany, whose reconcile
called resolveWorkspace() (which throws) before the detached-handle check.
Make reconcile resolve the workspace without throwing and treat an absent
workspace like an empty path — handing out a detached, local-only handle that
re-keys into a real entry once the workspace resolves. FlowWrapper then reuses
usePageDraftSync directly, keeping one code path for the page and SDK editors.
Seed via the spec's defaultValue (threaded through usePageDraftSync ->
useReactive -> useMany, captured once on first acquire and swallowed by the
syncer's seed guard) rather than a manual first-write effect, dropping the
fragile skipNextWrite assumption and the seededPath bookkeeping.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): wire DB-backed autosave into the whitelabel script SDK
ScriptWrapper had the same gap FlowWrapper did: ScriptBuilder delegates its
draft handle to the page (it only stop/restart-syncs and flushes by
userDraftPath), so the SDK's plain `bind:script` never reached a UserDraft
handle — no autosave, no indicator. Back it with usePageDraftSync<script>
(bind:script={draftSync.draft}, userDraftPath), seeded from the consumer's
script via defaultValue. Same one-code-path reuse as the flow SDK.
AppWrapper needs no change: AppEditor already self-acquires its handle
(UserDraft.use('app', ...)), so apps autosave already — and now also tolerate
mounting before login via the reconcile change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): gate SDK editors on a resolved workspace
Before a workspace exists the draft handle is detached (local-only); editing
into it and then having the workspace resolve re-keys to a fresh real entry
seeded from the original value, silently dropping those edits. Gate the flow,
script, and app SDK editors on `$workspaceStore` so no editing happens until
the real draft key exists. Embedders set the workspace before rendering (React
SDK initializeClients); the test_dev header sets it on mount.
AppEditor additionally acquires its handle at init from a non-reactive
workspace, so gating AppWrapper also ensures it mounts with the workspace
already set rather than permanently detached.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): add sdk_app test_dev page for the app editor SDK
Exercises AppWrapper the same way sdk_flow/sdk_script exercise their editors,
under the shared TestDevHeader. Confirms the app editor's self-managed autosave
+ AutosaveIndicator work via the SDK wrapper.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Points LATEST_GIT_SYNC_SCRIPT_PATH at the republished sync-script-to-git-repo
(windmill-labs/windmill-integrations#155) pinning windmill-cli@1.728.1, which
carries the gitSyncIncludePattern __mod/** fix (#9606). On-deploy git-sync was
running windmill-cli@1.713.2 and filtered workflow-as-code (WAC v2 / module)
scripts stored under <path>__mod/ out of the deploy pull, so they never reached
the repo.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: zero-setup oauth client credentials for registry-declared providers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: support client-credentials-only custom oauth providers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: add coupa client credentials provider to oauth registry
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: clarify oauth resource connect auth-method selection
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: support shared instance-level oauth client credentials
Admins can designate an instance OAuth entry's credentials as client
credentials; the connect dialog then runs the exchange server-side with
them instead of asking each user for their own. Replaces the per-provider
"Support Client Credentials Flow" toggle with a grant-type selector.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: update ee-repo-ref to be9f23b2c06b8b6ee0cd3e4d9f16bcd9e90129fb
This commit updates the EE repository reference after PR #613 was merged in windmill-ee-private.
Previous ee-repo-ref: 05643cbbc8c1bebf3509c691c5811b4057d96485
New ee-repo-ref: be9f23b2c06b8b6ee0cd3e4d9f16bcd9e90129fb
Automated by sync-ee-ref workflow.
* feat: allow both grant types on an instance oauth entry
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: bring-your-own oauth credentials from the others section
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: segmented oauth grant-type selector, always show grant
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: enable client credentials for 5 more oauth providers
Verified against official docs: bitbucket, linkedin, spotify, xero and
zoho support the standard client_credentials grant with a plain
client_id + client_secret, compatible with Windmill's token exchange.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: hide create-manually link on the managed oauth connect path
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: enable client credentials for salesforce and servicenow
Salesforce CC requires the org's My Domain token endpoint (login.salesforce.com
is unsupported for that grant), so add an optional cc_token_url registry field
that the connect form prefills for the client-credentials path instead of the
shared token_url. ServiceNow uses the same instance host for both grants, so it
only needs its token URL and req_body_auth surfaced at the top level.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: add instance-level client-credentials token url override
Some providers use a per-org/instance-specific token endpoint for the
client-credentials grant that differs from the authorization-code URL.
Add an optional cc_token_url on the instance OAuth entry, surfaced in
instance settings (prefilled from the registry template) when client
credentials is selected, and used for the CC exchange and refresh while
auth-code keeps its own token URL.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* style: remove redundant grant-type tags from oauth auth cards
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor: extract reusable RadioCard component for the oauth auth chooser
A token-based selectable card (label, description, selected, onSelect,
optional icon) replacing the inline cards in the connect dialog.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: hide sign-in option on the bring-your-own oauth path
Picking a provider from "Others" means bring your own credentials, so
the auth-code "Sign in" card (which uses the instance client) no longer
shows there — it goes straight to the client-credentials form. The
two-flow chooser stays on the instance-configured path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: restrict client-credentials token url to caller-supplied creds
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: resolve client-credentials id and secret all-or-nothing
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: host-pin client-credentials token url via instance-name input
For registry providers whose CC token URL is instance-templated (Coupa,
Salesforce My Domain, ServiceNow), the connect dialog and instance settings
collect an instance name and the backend substitutes it into the fixed-host
template, validating it as a hostname label. A free-form token URL is no longer
accepted for these providers, so the exchange host cannot be redirected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: client-credentials token url always comes from the registry
Bring-your-own CC is registry-only: the token URL is resolved server-side from
the built-in registry (host-pinned via an instance name for templated providers,
the fixed registry URL otherwise) and rejected for custom resource types. The
caller-supplied token URL field is removed from the connect dialog and the API.
Adds unit tests for the resolver.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: address CC review - sandbox CC config and instance-templated providers
Resolve `_sandbox` provider keys to the parent registry entry in the instance
settings and connect-dialog helpers, so salesforce_sandbox (and future sandbox
entries) can enable client credentials. Use the effective CC token URL template
(cc_token_url or token_url) so the instance-name field works for Coupa/ServiceNow,
and hide that field when a connect_config_template already owns the instance input
(ServiceNow). Document the authorization contract on resolve_instance_cc_credentials.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor: unify instance-templated oauth onto connect_config_template
Remove the separate cc_token_url and cc_instance config fields. An instance-
templated provider now declares one connect_config_template (auth_url optional
for client-credentials-only providers like Coupa); the CC flow reads its token
URL, label and strip_suffix to host-pin the exchange. Coupa and ServiceNow move
to connect_config_template; Coupa stays drawer-only (no auth_url -> excluded from
instance settings). Salesforce CC is removed for now (its auth-code/CC host split
needs the endpoint-profiles model).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: cc_scopes defaults and instance config for client credentials
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: store empty auth_url for cc-only templated oauth providers
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: address review nits - sandbox key lookup, template doc, deref specs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: default shared client-credentials connect to cc_scopes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: support bring-your-own client credentials for instance-configured providers
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor: move oauth grant-type help into per-option tooltips
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: keep instance-configured oauth providers selectable from Others
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: preserve admin-configured scopes for custom client-credentials providers
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: use cc scopes on cc refresh and enforce cc grant for bring-your-own
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: require {instance} in leftmost host label for cc token url templates
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: drop token_url from unauthenticated get_connect response
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: fill byo templated resource args from the entered instance
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 136f4634aca61e74ccb045372358a1e3f6b23e75
This commit updates the EE repository reference after PR #616 was merged in windmill-ee-private.
Previous ee-repo-ref: b5083e266492e908456e39401778a9cdcea46e94
New ee-repo-ref: 136f4634aca61e74ccb045372358a1e3f6b23e75
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Previously the AI sessions sidebar section was hidden entirely when AI was
not configured at the workspace level. Now the section stays visible and the
per-session chat input is disabled with an explanatory message, mirroring the
sidebar AI chat behavior.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(cli): improve generate-metadata guidance, fix description parser
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(cli): surface dependency version bumps after generate-metadata
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(cli): explain generate-metadata scope, import cascade, and --dry-run troubleshooting
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The localStorage→DB user-draft migration upserted via /drafts/update, whose
SQL always stamped created_at = now(). Every migrated draft therefore
resurfaced to the top as freshly created, regardless of its real age.
Add an optional created_at override to the update_draft request, threaded
into the upsert as COALESCE($8, now()) / created_at = EXCLUDED.created_at.
Normal saves omit it and still stamp now(); the migration passes the draft's
original write time (or epoch 0 when unknown) so migrated drafts keep their age.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three issues from the Codex PR review of the low-code app deploy + summary work:
- [P1] The summary mirror onto the autosaved App value broke the autosave's
no-op detection for deployed apps: `discardIf` compares the live value against
the deployed baseline, but the baseline (the deployed App value) carried no
summary while the live value now always does — so a draft reverted to the
deployed state never compared equal and a no-op draft was persisted instead of
deleted. Carry the deployed summary onto the baseline so the comparison matches
(a summary-only edit still counts as a real change).
- [P2] "Show diff" stayed enabled for view-only (`mine=false`) rows in the
"Show all drafts" view, but the diff only fetches the current user's draft
overlay — wrong diff for another user's deployed-row draft, 404 for their
draft-only row. Hide it for foreign rows; own/legacy rows keep it.
- [P2] Reword the `rawAppDraftValue` doc comment to state the current invariant
(must read a draft's top-level `files`) instead of referencing past drafting
history, per AGENTS.md.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: per-user draft gating, badges and rename display on deploy page
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): don't strike the path when a draft adds a summary to a summary-less item
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): don't strike draft-only items' auto-generated path against the pretty path
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): deploy raw-app drafts from top-level files so the bundle isn't dropped
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(frontend): share raw-app source→draft-value projection across chat and deploy page
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): deploy renamed/new flow, app and raw-app drafts at draft_path, not the temp storage path
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(frontend): add a design-system Checkbox and use it for deploy-page row/select-all checkboxes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: "Show all drafts" toggle on the deploy-drafts page
Replace the deploy-drafts page's legacy-hiding "Only my drafts" toggle with
a "Show all drafts" toggle that switches the listing scope between the
current user's own drafts (+ legacy no-owner rows) and every user's drafts
in the workspace.
Backend (`drafts.rs`, `openapi.yaml`):
- `/drafts/list` gains an `all_users` query param that drops the owner
filter, and a per-row `mine` flag (own draft or legacy no-owner row).
`DISTINCT ON` now prefers the user's own row, then the legacy row, then
another user's, so `mine`/`legacy_draft` describe the kept row.
Frontend (`CompareDrafts.svelte`, `workspaceDrafts.svelte.ts`):
- "Show all drafts" toggle (default off). The all-users superset is fetched
lazily via the shared resource only while the toggle is on, so the page's
fork draft-count (own drafts) is unaffected.
- Other users' drafts are view-only: disabled checkbox + Discard with a
"belongs to another user" tooltip; Show diff stays enabled. Selection,
select-all and the deploy count only ever include the user's own drafts.
The multi-user warning triangle shows on owned rows only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(backend): gate all_users draft listing by read permission
Addresses the PR review on the per-user deploy-drafts page:
- `/drafts/list?all_users=true` previously had only `WHERE workspace_id = $1`
with no read-permission check, so any non-operator could enumerate every
draft's path, summary and authors — including items they can't read. Now
rows the caller doesn't own (`mine = false`) are gated through
`require_can_read_path` (the same gate `/drafts/get` uses) and dropped when
unreadable; both its `NotFound` and `NotAuthorized` denials are treated as
"not visible".
- Skip the per-row `require_can_write_path` probe on those non-owned rows
(they're never selectable — `isSelectable` requires `mine`): set
`can_write = false` directly, removing a redundant N RLS write-probes when
`all_users` is on.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): only confirm destructive draft discards on the deploy page
Discarding a draft is non-destructive in every case except removing the last
draft of a never-deployed item (`draft_only` with no other user's draft),
which permanently deletes it. Confirm only that case; reverting a draft over a
deployed item, or discarding your copy while another user still holds a draft,
now runs immediately (the ⚠️ already signals the multi-user case). Drops the
redundant "other users still have a draft" / "deployed version unaffected"
confirmation branches.
Harden the destructive check: it keyed off `otherDraftUsers()`, which subtracts
`currentUsername`; while `$userStore.username` is unhydrated, your own draft
looked like another user's, flipping a draft-only item to "non-destructive" and
deleting it with no confirmation. Now: deployed counterpart → never destructive;
`draft_only` with unknown `currentUsername` → treated as destructive (confirm).
The delete modal also shows the friendly `draft_path` instead of the raw
`draft_{uuid}` storage path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): deploy low-code app drafts (value + summary persistence)
A visual (low-code) app draft is autosaved as the *bare* App value
(grid/theme/... plus a draft-only `draft_path`), not wrapped in
{ value, summary, policy } like script/flow drafts. The Review & Deploy page
read `requestBody.value = d.value` — undefined for that shape — so deploying any
low-code app draft (created or edited) sent no value and failed. Read the value
from the draft object itself, strip the draft-only `draft_path` from it, and use
that as the deploy path.
Also persist the app summary, which was dropped entirely: the autosave stores
the bare App value (the summary normally lives only in the `app` table column,
set on deploy), so a draft never carried it — reopening a draft or deploying it
lost the summary. Mirror the summary onto the autosaved App (like `draft_path`),
read it back when loading a draft, and on deploy send it as the summary column
while stripping it (and `draft_path`) from the deployed value so the value stays
clean.
Verified end-to-end: a new low-code app with a summary deploys at its pretty
path with the summary set, content intact, and no draft_path/summary leaked into
the deployed value; the draft is cleaned up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): queue messages typed while ai chat is streaming
* fix(frontend): avoid losing queued chat messages on send early-return
* test(frontend): cover queued chat message semantics in AIChatManager
* fix(frontend): complete ChatLoopResult mock in queued message tests
* feat(frontend): single appendable queued message, send on cancel
* fix(frontend): only auto-send queued message on a user cancel, not programmatic
* chore(frontend): remove queued-message dev preview page
* fix(frontend): clear queued chat message on conversation switch
* **apps:** opt-in sandbox isolation for published & raw apps (alpha) ([#9420](https://github.com/windmill-labs/windmill/issues/9420)) ([2879cbb](https://github.com/windmill-labs/windmill/commit/2879cbb65a4122c86b4a472206d74d9009b07904))
* **frontend:** destroy old WebsocketProvider on workspace switch in MultiplayerMenu ([#9719](https://github.com/windmill-labs/windmill/issues/9719)) ([6e96f90](https://github.com/windmill-labs/windmill/commit/6e96f90065dfe2f6ccc5eb4f85f4facd3515c70c))
* **frontend:** ensure type:object in test_run_flow tool schema for Anthropic ([#9721](https://github.com/windmill-labs/windmill/issues/9721)) ([d5cb944](https://github.com/windmill-labs/windmill/commit/d5cb944cf92f074b2ee42c876595eacdfa2f4d76))
* **health:** detect read-only replica via pg_is_in_recovery() ([#9722](https://github.com/windmill-labs/windmill/issues/9722)) ([e16061d](https://github.com/windmill-labs/windmill/commit/e16061df06babeae935a9396de5bdcd46e8119a9))
* re-enforce scoped API token boundaries across handlers ([#9712](https://github.com/windmill-labs/windmill/issues/9712)) ([e19594d](https://github.com/windmill-labs/windmill/commit/e19594df2ad015a0336ade95e04562f5562ec3f6))
* link files & folders to the global AI chat ([#9520](https://github.com/windmill-labs/windmill/issues/9520)) ([84cc043](https://github.com/windmill-labs/windmill/commit/84cc043406d63a1e1472165cf24ce8c09905fc5f))
* scope default instance db name to workspace (dt_/dl_) ([#9699](https://github.com/windmill-labs/windmill/issues/9699)) ([4a8a724](https://github.com/windmill-labs/windmill/commit/4a8a724895dcecb835e1eb1e4fd7d1bbc8b3e0fb))
### Bug Fixes
* enforce job_dir containment when writing module files ([#9703](https://github.com/windmill-labs/windmill/issues/9703)) ([e403f92](https://github.com/windmill-labs/windmill/commit/e403f92d7e84cebc78709dce1a0928048ba2506d))
* **frontend:** deploy full script/flow draft from AI chat via shared module ([#9642](https://github.com/windmill-labs/windmill/issues/9642)) ([23bf6bf](https://github.com/windmill-labs/windmill/commit/23bf6bf3da01d552d2dfab6dbcfd30f758ed34d2))
* ignore NotFound errors when deleting log files from object store ([#9707](https://github.com/windmill-labs/windmill/issues/9707)) ([8a0b0ab](https://github.com/windmill-labs/windmill/commit/8a0b0abead71320c4f69eb3007739a19f76d4126))
* **oauth:** restore bring-your-own CC token URL override ([#9711](https://github.com/windmill-labs/windmill/issues/9711)) ([ef4962e](https://github.com/windmill-labs/windmill/commit/ef4962e52aba0bc79bf72523de9101853c660654))
* sanitize git credentials from ansible executor errors and logs ([#9697](https://github.com/windmill-labs/windmill/issues/9697)) ([ace7b68](https://github.com/windmill-labs/windmill/commit/ace7b68a28b00d715298ffcb6ae907c1974a74b8))
* ducklake materialization for data pipelines ([#9689](https://github.com/windmill-labs/windmill/issues/9689)) ([3ebf243](https://github.com/windmill-labs/windmill/commit/3ebf24359d66048d6361ce65cd879cdc04b737ed))
### Bug Fixes
* **frontend:** clear branch step state when switching outer loop iterations ([#9650](https://github.com/windmill-labs/windmill/issues/9650)) ([09a8004](https://github.com/windmill-labs/windmill/commit/09a80040ca268a4379d5302e3401435ba93247e0))
* **frontend:** group live pipeline runs in the activity panel ([#9684](https://github.com/windmill-labs/windmill/issues/9684)) ([1be4df9](https://github.com/windmill-labs/windmill/commit/1be4df9acb935250d4cc12e83cf67e366d870d5a))
* require super admin for object storage config test endpoint ([#9683](https://github.com/windmill-labs/windmill/issues/9683)) ([fb44fe7](https://github.com/windmill-labs/windmill/commit/fb44fe7af2b8ebe8ef64ffb0e5acce8580bf4200))
* validate websocket trigger urls and gate trigger test route ([#9682](https://github.com/windmill-labs/windmill/issues/9682)) ([c39ee07](https://github.com/windmill-labs/windmill/commit/c39ee07c0bcd2249dd19ffa5cd988125eefc6c9f))
* **ai-chat:** cap read_app_file + search_app grep tool to bound context in large raw apps ([#9653](https://github.com/windmill-labs/windmill/issues/9653)) ([4296a6a](https://github.com/windmill-labs/windmill/commit/4296a6ae1f73564de4df54fe1df0a03c1df05dfd))
* **python, windows:** enable S3 to cache wheels ([#5199](https://github.com/windmill-labs/windmill/issues/5199)) ([ab3bc97](https://github.com/windmill-labs/windmill/commit/ab3bc97cd92b6480327029bcf018280442462af7))
### Bug Fixes
* allow users to always discard their own drafts without write permission ([#9659](https://github.com/windmill-labs/windmill/issues/9659)) ([6833a55](https://github.com/windmill-labs/windmill/commit/6833a554aeddb3e63173d3c3140b490c0bf2822b))
* **backend:** clean up unique_ext_jwt_token on workspace deletion ([#9676](https://github.com/windmill-labs/windmill/issues/9676)) ([9add719](https://github.com/windmill-labs/windmill/commit/9add719d936cdcfb2c4062629e3e1f792694dafe))
* **backend:** strip NUL bytes from draft values on write ([#9673](https://github.com/windmill-labs/windmill/issues/9673)) ([924f9c7](https://github.com/windmill-labs/windmill/commit/924f9c7e8d8863d9af40aee246a519b4be0e1ea2))
* **python:** split PIP_TRUSTED_HOST by whitespace to support multiple hosts ([#9675](https://github.com/windmill-labs/windmill/issues/9675)) ([cafb473](https://github.com/windmill-labs/windmill/commit/cafb473494d9cff3a8b2aeaf9f18b015f966e7b3))
* **ansible:** add AI chat and editor bar buttons for ansible ([#9671](https://github.com/windmill-labs/windmill/issues/9671)) ([017c3d3](https://github.com/windmill-labs/windmill/commit/017c3d3343c2577501103be4b2dd8dac9727d80d))
* **backend:** grant script_trigger access to windmill roles ([#9674](https://github.com/windmill-labs/windmill/issues/9674)) ([3361736](https://github.com/windmill-labs/windmill/commit/33617367d09537667d2ab3f91135c736194b9e7e))
* **frontend:** ignore hash/assets in script diffs and drafts (WIN-2071) ([#9664](https://github.com/windmill-labs/windmill/issues/9664)) ([3371265](https://github.com/windmill-labs/windmill/commit/33712653821e83f2562dd5f271dbec0188d5d2f8))
* **backend:** grant notify_event access to windmill roles ([#9665](https://github.com/windmill-labs/windmill/issues/9665)) ([a682d02](https://github.com/windmill-labs/windmill/commit/a682d02311a2110bfc0d5e0a5b52e96147fe0dd7))
* **mcp:** repair invalid type keywords in tool JSON schemas ([#9667](https://github.com/windmill-labs/windmill/issues/9667)) ([c30bdec](https://github.com/windmill-labs/windmill/commit/c30bdecea77ff9b4d74d52961f3101201099b683))
* Data Pipelines alpha ([#9193](https://github.com/windmill-labs/windmill/issues/9193)) ([7155a0b](https://github.com/windmill-labs/windmill/commit/7155a0bb96cf30bd878272a0f4c3c3b02341b261))
### Bug Fixes
* **ai-chat:** stop echoing app draft value in global chat write tool results ([#9658](https://github.com/windmill-labs/windmill/issues/9658)) ([2fed808](https://github.com/windmill-labs/windmill/commit/2fed808b9e716d9a44b34c7a073ec0d37374be05))
* **backend:** include raw_app drafts in list_apps draft_users ([#9647](https://github.com/windmill-labs/windmill/issues/9647)) ([19bc005](https://github.com/windmill-labs/windmill/commit/19bc0052f1069d732231950a0ec958f675d57417))
* **frontend:** keep ?new_draft flag until first save is confirmed ([#9656](https://github.com/windmill-labs/windmill/issues/9656)) ([9b6b7c3](https://github.com/windmill-labs/windmill/commit/9b6b7c3862d9988e5e91eaab2b967a23f41cdc0d))
* **frontend:** re-key raw-app autosave on post-deploy navigation ([#9646](https://github.com/windmill-labs/windmill/issues/9646)) ([1058bde](https://github.com/windmill-labs/windmill/commit/1058bdeccdc4c403ef4599db0ee74a65a66c715f))
* gate agent-worker global setting reads with a blocklist ([#9623](https://github.com/windmill-labs/windmill/issues/9623)) ([fdd82f0](https://github.com/windmill-labs/windmill/commit/fdd82f0c48f29805cd9e219649f27fba45c7fd92))
* add ducklake schema support to the database manager ([#9633](https://github.com/windmill-labs/windmill/issues/9633)) ([3eeccaf](https://github.com/windmill-labs/windmill/commit/3eeccaf9682b7803fdf5be8dcbc4d243e0ba2e49))
* **frontend:** View Diff and in-place Load for other users' drafts ([#9621](https://github.com/windmill-labs/windmill/issues/9621)) ([5508f1d](https://github.com/windmill-labs/windmill/commit/5508f1da9cd04c2583eb3f7ee6bce19d067f2227))
* queue messages typed while ai chat is streaming ([#9525](https://github.com/windmill-labs/windmill/issues/9525)) ([51bd869](https://github.com/windmill-labs/windmill/commit/51bd8692a482850f7ac8b04dd16db5876336b5b9))
* zero-setup oauth client credentials for registry providers ([#9559](https://github.com/windmill-labs/windmill/issues/9559)) ([e26a923](https://github.com/windmill-labs/windmill/commit/e26a9239a62a25abf90ef06ade4dde7f36e791bb))
### Bug Fixes
* **ai_evals:** adapt global eval harness to DB-backed user drafts ([#9641](https://github.com/windmill-labs/windmill/issues/9641)) ([e87ff79](https://github.com/windmill-labs/windmill/commit/e87ff79ecf6a6e0958916ed1b3756fb3addf719f))
* **drafts:** preserve original timestamp when migrating localStorage drafts ([#9638](https://github.com/windmill-labs/windmill/issues/9638)) ([8021775](https://github.com/windmill-labs/windmill/commit/8021775f5f961ef6fd01b022639b85855326a1da))
* **frontend:** don't save drafts on leave when auto-save is off, warn instead ([#9630](https://github.com/windmill-labs/windmill/issues/9630)) ([2523465](https://github.com/windmill-labs/windmill/commit/252346500945a9571af744c839ac0c7d6870504f))
* **frontend:** render Modal2 dialogs above the AI chat panel ([#9636](https://github.com/windmill-labs/windmill/issues/9636)) ([b67c8cf](https://github.com/windmill-labs/windmill/commit/b67c8cf42b477575fc1bc448058ec0d3b7e54fee))
* **frontend:** show AI sessions when AI unconfigured, with disabled chat ([#9644](https://github.com/windmill-labs/windmill/issues/9644)) ([ba69d81](https://github.com/windmill-labs/windmill/commit/ba69d8147b615e160cf3d2885fc65a0777b78b71))
* **git-sync:** bump default sync script to hub/28719 (windmill-cli 1.728.1) for WAC modules ([#9649](https://github.com/windmill-labs/windmill/issues/9649)) ([3c0e38b](https://github.com/windmill-labs/windmill/commit/3c0e38b5890d77983cb5cf5f422a62a73e7a4f22))
-`--models <a,b,c>`: run the same cases sequentially against several model aliases
-`--verbose`: stream assistant output for frontend runs
-`--skip-judge`: skip LLM judge scoring for the run
-`--execution-only`: only require the model/proxy/frontend loop to complete; skip validators, tool expectations, backend artifact validation, and judge scoring
-`--record`: append a compact tracked summary line to `ai_evals/history/<mode>.jsonl` for full-suite runs only
-`--backend-validation <mode>`: optional backend smoke validation (`off` or `preview`) for `script` and `flow` evals
@@ -99,7 +101,7 @@ Notes:
- the command also prints accepted alias spellings such as `gpt-4o`, `gpt-55`, `claude-opus-4.6`, and `claude-haiku-4.5`
- frontend modes (`flow`, `script`, `app`, `global`) can use Anthropic, OpenAI, Gemini, and DeepSeek-backed aliases
-`cli` mode always uses the Anthropic agent SDK, so only Anthropic aliases are valid there
- the judge model is separate and currently defaults to `claude-sonnet-4-6`
- the judge model is separate and currently defaults to `claude-sonnet-4-6`; use `--skip-judge` for deterministic-only runs
"query":"SELECT\n COUNT(*)::bigint AS \"total!\",\n COUNT(*) FILTER (WHERE name = ANY($2::text[]))::bigint AS \"replacing!\"\n FROM ai_skill\n WHERE workspace_id = $1",
"query":"WITH del AS (\n DELETE FROM asset WHERE workspace_id = $1 AND usage_path = $2 AND usage_kind = $3\n RETURNING usage_access_type\n )\n INSERT INTO notify_event (channel, payload)\n SELECT 'notify_asset_producer_change', $1\n WHERE $3 = 'script'\n AND EXISTS (SELECT 1 FROM del WHERE usage_access_type IN ('w', 'rw'))",
"query":"WITH del AS (\n DELETE FROM asset WHERE workspace_id = $1 AND usage_kind = 'script'\n AND usage_path = (SELECT path FROM script WHERE hash = $2 AND workspace_id = $1)\n RETURNING usage_access_type\n )\n INSERT INTO notify_event (channel, payload)\n SELECT 'notify_asset_producer_change', $1\n WHERE EXISTS (SELECT 1 FROM del WHERE usage_access_type IN ('w', 'rw'))",
"query":"\n SELECT kind, path, script_path, is_flow FROM (\n SELECT 'schedule' AS kind, path, script_path, is_flow FROM schedule\n WHERE workspace_id = $1\n AND script_path IS NOT NULL\n UNION ALL\n SELECT 'email', path, script_path, is_flow FROM email_trigger\n WHERE workspace_id = $1\n UNION ALL\n SELECT 'kafka', path, script_path, is_flow FROM kafka_trigger\n WHERE workspace_id = $1\n UNION ALL\n SELECT 'mqtt', path, script_path, is_flow FROM mqtt_trigger\n WHERE workspace_id = $1\n UNION ALL\n SELECT 'nats', path, script_path, is_flow FROM nats_trigger\n WHERE workspace_id = $1\n UNION ALL\n SELECT 'postgres', path, script_path, is_flow FROM postgres_trigger\n WHERE workspace_id = $1\n UNION ALL\n SELECT 'sqs', path, script_path, is_flow FROM sqs_trigger\n WHERE workspace_id = $1\n UNION ALL\n SELECT 'gcp', path, script_path, is_flow FROM gcp_trigger\n WHERE workspace_id = $1\n ) t\n WHERE ($2::text IS NULL OR script_path LIKE $2)\n ",
"query":"DELETE FROM job_perms\n WHERE ctid IN (\n SELECT jp.ctid FROM job_perms jp\n WHERE NOT EXISTS (SELECT 1 FROM v2_job_queue q WHERE q.id = jp.job_id)\n LIMIT 100000\n )",
"query":"SELECT\n subscriber_path AS \"subscriber_path!\",\n asset_kind AS \"asset_kind!: windmill_common::assets::AssetKind\",\n asset_path AS \"asset_path!\",\n outcome::text AS \"outcome!\",\n child_job_id,\n partition,\n received_inputs,\n required_inputs,\n debounce_s,\n reason,\n created_at AS \"created_at!\"\n FROM dispatch_event\n WHERE producer_job_id = $1 AND workspace_id = $2\n ORDER BY id",
"query":"\n DELETE FROM asset\n WHERE id IN (\n SELECT id FROM (\n SELECT a.id, ROW_NUMBER() OVER (\n PARTITION BY a.workspace_id, a.path, a.kind\n ORDER BY a.created_at DESC\n ) as rn,\n limits.max_n\n FROM asset a\n INNER JOIN (\n SELECT * FROM UNNEST(\n $1::varchar[],\n $2::varchar[],\n $3::asset_kind[],\n $4::int[]\n ) AS t(workspace_id, path, kind, max_n)\n ) limits\n ON a.workspace_id = limits.workspace_id\n AND a.path = limits.path\n AND a.kind = limits.kind\n WHERE a.usage_kind = 'job'\n ) ranked\n WHERE rn > max_n\n )",
"query":"SELECT count(DISTINCT trigger_ref) AS \"n!\"\n FROM join_pending_inputs\n WHERE workspace_id = $1 AND subscriber_path = $2 AND partition = $3",
"query":"DELETE FROM join_pending_inputs jpi\n USING (\n SELECT workspace_id, subscriber_path, partition\n FROM join_pending_inputs\n GROUP BY workspace_id, subscriber_path, partition\n HAVING max(received_at) <= now() - ($1::bigint::text || ' s')::interval\n ) stale\n WHERE jpi.workspace_id = stale.workspace_id\n AND jpi.subscriber_path = stale.subscriber_path\n AND jpi.partition = stale.partition",
"query":"SELECT count(DISTINCT trigger_ref) AS \"n!\"\n FROM script_trigger\n WHERE workspace_id = $1\n AND runnable_path = $2\n AND trigger_kind = 'asset'\n AND runnable_kind = 'script'\n AND trigger_ref LIKE '%' || $3 || '%'",
"query":"DELETE FROM job_result_stream_v2\n WHERE job_id NOT IN (SELECT id FROM v2_job_queue)\n AND job_id NOT IN (\n SELECT id FROM v2_job_completed\n WHERE completed_at > NOW() - INTERVAL '60 seconds'\n )\n RETURNING job_id",
"query":"WITH legacy AS (\n DELETE FROM draft\n WHERE workspace_id = $1 AND path = $2 AND typ = $3 AND email IS NULL\n RETURNING value\n )\n INSERT INTO draft (workspace_id, email, path, typ, value, created_at)\n SELECT $1, $4, $2, $3, value, now() FROM legacy\n ON CONFLICT (workspace_id, path, typ, email) WHERE email IS NOT NULL\n DO UPDATE SET value = EXCLUDED.value, created_at = now()\n RETURNING 1 as \"one!\"",
"query":"\n SELECT\n asset.kind AS \"asset_kind!: AssetKind\",\n asset.path AS \"asset_path!\",\n asset.usage_kind AS \"usage_kind!: AssetUsageKind\",\n asset.usage_path AS \"usage_path!\",\n asset.usage_access_type::text AS \"access_type\"\n FROM asset\n WHERE asset.workspace_id = $1\n AND asset.usage_kind IN ('script', 'flow')\n AND ($2::asset_kind[] IS NULL OR asset.kind = ANY($2))\n AND ($3::text IS NULL OR asset.usage_path LIKE $3)\n GROUP BY asset.kind, asset.path, asset.usage_kind, asset.usage_path, asset.usage_access_type\n ",
"query":"\n SELECT\n usage_path AS \"usage_path!\",\n kind AS \"kind!: AssetKind\",\n path AS \"path!\"\n FROM asset\n WHERE workspace_id = $1\n AND usage_kind = 'script'\n AND usage_access_type IN ('w', 'rw')\n ",
"query":"SELECT value as \"value!: sqlx::types::Json<Box<serde_json::value::RawValue>>\", created_at\n FROM draft\n WHERE workspace_id = $1 AND path = $2 AND typ = $3 AND email = $4",
"query":"SELECT runnable_path AS \"runnable_path!\", kind::text AS \"kind!\"\n FROM v2_job\n WHERE workspace_id = $1 AND trigger_kind = 'asset'\n ORDER BY runnable_path",
"query":"\n SELECT\n runnable_kind AS \"runnable_kind!: AssetUsageKind\",\n runnable_path AS \"runnable_path!\",\n trigger_kind::text AS \"trigger_kind!\",\n trigger_ref AS \"trigger_ref!\"\n FROM script_trigger\n WHERE workspace_id = $1\n AND trigger_kind = 'asset'\n AND ($2::text IS NULL OR runnable_path LIKE $2)\n ",
"query":"SELECT q.runnable_settings_handle\n FROM v2_job j JOIN v2_job_queue q ON q.id = j.id\n WHERE j.workspace_id = $1 AND j.runnable_path = $2\n AND j.trigger_kind = 'asset'",
"query":"\n SELECT runnable_path AS \"runnable_path!\", join_all AS \"join_all!\", debounce_s,\n retry_count, retry_delay_s\n FROM script_trigger\n WHERE workspace_id = $1\n AND trigger_kind = 'asset'\n AND trigger_ref = $2\n AND runnable_kind = 'script'\n ",
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.