* feat(telemetry): disclose resource and infra usage stats
When minimal telemetry is disabled, the stats payload now includes resource
counts (workspaces, scripts per language, flows, workflows as code, low-code
and raw apps) and, on EE only, infrastructure info (container runtime,
database size, max connections, RDS detection).
Update the telemetry disclosure in instance settings accordingly: resource
counts are listed for both CE and EE; infra info is shown only on EE since it
is collected only there. Bump the EE ref and add the sqlx cache for the new
queries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(telemetry): expand EE infra disclosure and add sysinfo dep
Disclose the expanded EE infrastructure telemetry (deployment mode, host
OS/arch/CPU/memory, filesystem space, Postgres version and connection counts,
object storage backend, sandboxing and retention settings) in instance
settings. Add sysinfo as a windmill-common dependency for host memory and
filesystem stats, bump the EE ref, and add the sqlx cache for the new queries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(telemetry): focus EE infra disclosure on wrapping platform
Drop the single-server host details (OS, arch, CPU, memory, filesystem) and
tuning config from the EE infra disclosure, and revert the sysinfo dependency
they required. Reflect managed-database-provider detection in place of the RDS
flag. Bump the EE ref and update the sqlx cache for the revised queries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(telemetry): drop deployment mode and worker count from disclosure
Remove deployment mode and worker count from the EE infra disclosure to match
the backend, and bump the EE ref. They reflect only the node sending telemetry,
not the deployment topology.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 6d3301507db50818f1683dac3941d3e0cf1152a7
This commit updates the EE repository reference after PR #627 was merged in windmill-ee-private.
Previous ee-repo-ref: d30e7d18d14992598a97356d0ed13f7d5d585115
New ee-repo-ref: 6d3301507db50818f1683dac3941d3e0cf1152a7
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>
* fix: allow non-super-admin object storage test, harden SSRF surface on cloud
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: validate effective object storage host to close region/bucket SSRF bypass
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: validate gcs_base_url/token_uri in GCS service account key to close SSRF bypass
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: match url scheme case-insensitively in object storage host validation
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 expired-job retention loop re-scanned the same oldest rows on every batch. When
the oldest completed jobs are undeletable (children of a still-active root flow), the
ORDER BY completed_at ASC scan walked that protected prefix on each of the up-to-20
batches, doing a v2_job PK lookup per row — quadratic in prefix size (measured ~9s/batch,
~180s/cleanup-cycle on a 1.5M-row prefix).
Carry a completed_at watermark (max deleted) across batches and re-apply it as
completed_at >= floor so each batch resumes past the already-processed prefix. Also skip
the v2_job join entirely when no old root flow is active (the common case), since nothing
is protected then. Measured: subsequent batches 9000ms -> 159ms; empty-set path 154 -> 36ms.
The watermark only ever skips rows the current run already deleted, was protecting, or
skip-locked — all deferred to the next run, identical to the unbounded scan's row set
(verified: union of batched deletes == single delete, 0 diff). Mirrored in
windmill-api-settings log_cleanup.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: reject pipeline `# tag` annotation false-positives on regular comments
`parse_pipeline_annotations` treats any comment line starting with
`# tag <text>` as a worker-tag annotation. In Python scripts, ordinary
English comments beginning with "# tag ..." were misinterpreted: values
over 50 chars failed the `script.tag` INSERT (varchar(50)), and shorter
ones silently overrode the script's worker tag.
Worker tags are single-word identifiers (e.g. `heavy`, `gpu`), so reject
any candidate that contains whitespace or exceeds 50 characters. Mirror
the same validation in the TS parity parser and add regression tests on
both sides.
Fixes WIN-2090
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: restrict pipeline annotation scan to the leading comment header
The root cause of the `# tag` false-positive is broader than the `tag`
keyword: `parse_pipeline_annotations` scanned every comment line in the
whole file, so any body comment matching an annotation grammar
(`on`, `freshness`, `tag`, `retry`, ...) was misinterpreted. The `tag`
case was the most visible because an over-length value crashed the
`script.tag` INSERT (varchar(50)).
Windmill's other comment-directive parsers (BashAnnotations::sandbox_image,
ssh_target) already scan only the leading comment header and stop at the
first line of real code. Align parse_pipeline_annotations (and its TS
mirror) with that convention: skip blank lines, break on the first
non-comment line. This eliminates body-comment false-positives for every
annotation, not just `tag`.
The `tag` whitespace/length guard from the previous commit is kept as
defense for prose that sits in the header itself.
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: 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>
* 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>
* 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>
* 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>
* 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: 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>
* 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>
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>
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 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>