Commit Graph

6641 Commits

Author SHA1 Message Date
Ruben Fiszel 43bb676dc5 fix: ping job during volume setup to prevent false zombie restarts (#9803)
* fix: ping job during volume setup to prevent false zombie restarts

Volume mount setup (S3 lease acquisition wait + download) runs synchronously
before the language executor spawns the child process and its ping loop,
leaving the job ping frozen. A slow lease wait or cold S3 download could
exceed ZOMBIE_JOB_TIMEOUT (default 60s) and get the job falsely restarted as
a zombie. Heartbeat the job ping throughout volume setup.

EE companion: windmill-labs/windmill-ee-private#633

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to 7b92c8e0de4cfc6d986499d60a5f79cd1c6b9d0b

This commit updates the EE repository reference after PR #633 was merged in windmill-ee-private.

Previous ee-repo-ref: 32e6b9a25f4ec3ea87f429b3d6279f9287a24de7

New ee-repo-ref: 7b92c8e0de4cfc6d986499d60a5f79cd1c6b9d0b

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>
2026-06-26 01:51:38 +02:00
Ruben Fiszel b7a227f860 chore(main): release 1.740.0 (#9776)
* chore(main): release 1.740.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-06-25 21:37:57 +00:00
Ruben Fiszel 0dbd9c1231 perf: eliminate dual-connection DB pool contention across worker, queue, and api (#9798)
* perf: eliminate dual-connection DB pool contention across worker, queue, and api

Reuse the held transaction (or move pool reads before begin()) instead of
checking out a second pool connection while a tx is open, extending the
fix from #9789/#7861. Targets the per-worker pool (max 5) hot paths plus
several server-pool API handlers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: pass owned pool to get_email_from_permissioned_as in http trigger handler

The generified signature takes impl PgExecutor; the http trigger handler
passed &db where db is already &DB, yielding &&Pool which does not impl
PgExecutor (only surfaced under the full feature set in CI).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: keep RLS-exposed reads on the non-RLS pool and isolate flow-eval reads in a savepoint

Addresses review of the dual-connection sweep:

- worker_flow: wrap the stop_after_all_iters_if reads in a SAVEPOINT. The
  caller swallows the error and keeps using tx, so a DB read failure must
  not leave the outer transaction aborted (it would fail the later commit).
  Matches the previous pool-read semantics.

- Revert reads that were moved onto an RLS (user_db) transaction back to the
  non-RLS pool, since RLS row-visibility/role context can change results:
  push_scheduled_job (email/tag/settings lookups; reachable with a user_db
  tx from api-schedule/api-flows), push_inner native-retry dedicated_worker
  routing (RLS isolation variants), resources.rs app-namespace folder
  auto-create (non-admins must not be blocked), and the script archive/delete
  UPDATEs. Non-RLS db.begin() reuse and move-before-begin are kept.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: failpoint proving the stop_after_all_iters_if savepoint isolates an aborted read

Adds a worker-crate failpoints feature and a data-driven hook: when the
stop_after_all_iters_if expr is the magic sentinel, the in-evaluation read runs
SELECT 1/0 to abort its (savepoint) transaction. The test asserts the flow still
completes (iteration marked failed) — which only holds if the savepoint keeps the
outer status-update transaction committable. Without the savepoint the abort would
poison the outer tx and the job would never complete.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 21:33:17 +00:00
Ruben Fiszel b28f974e50 fix: opt out of Deno minimum-dependency-age for private npm registries (#9802)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 20:08:52 +02:00
Ruben Fiszel ba768fee88 feat(api): add structured endpoint for flow logs (#9797)
Add `GET /w/{workspace}/jobs_u/get_flow_all_logs_structured/{id}` as a
JSON alternative to `get_flow_all_logs`. It returns the same flow log
tree as an array of per-job entries (job_id, label, kind, step path,
depth, parent module type, sibling index/count, and resolved logs)
instead of a single delimited text blob, so callers can render or
process logs per-step without parsing the `=== ... ===` markers.

The shared auth, recursive-CTE query, and label-building logic is
extracted into `collect_flow_log_entries`; the existing text endpoint
now formats those entries and produces byte-identical output.

Fixes WIN-2102

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:50:40 +00:00
Ruben Fiszel 5549bdc67a fix(debounce): never supersede a running debounce survivor (#9780)
* fix(debounce): never supersede a running debounce survivor

Companion to the windmill-ee-private change in upsert_debounce_key.

With debounce_args_to_accumulate + a concurrent_limit, a message arriving
while its debounce survivor is already running was marked completed/skipped
("Debounced Running by ...") and the running survivor deleted from the
queue, silently dropping accumulated elements. A slow step + concurrent
limit keeps the survivor running for a long window, so any arrival during
it was lost. The fix leaves a running survivor untouched and starts a fresh
debounce window for the late arrival.

Adds regression coverage in windmill-queue/tests/debounce_test.rs (push,
flow post-preprocessing, no-accumulation, committed-running, and
max-count-window cases) and refreshes the SQLx cache for the changed
upsert_debounce_key queries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(debounce): add missing SQLx cache for test-only running-flag query

The cargo_test CI job compiles the test target with SQLX_OFFLINE=true; the
new regression tests use `UPDATE v2_job_queue SET running = true ...` which
was not in the offline cache (the library-only `cargo sqlx prepare` skipped
test targets). check_oss/check_ee passed because they don't build tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(debounce): harden running-survivor guard against concurrent arrivals

Companion to windmill-ee-private: switch the running-state check to a
correlated EXISTS on the post-conflict-lock holder so two late arrivals
racing after a survivor started running can't both spawn independent
windows (the row lock serializes them; the second debounces into the
first's fresh window).

Adds a concurrent regression test
(test_debounce_concurrent_arrivals_after_running_survivor) asserting
exactly one late arrival survives and the other is debounced, and refreshes
the SQLx cache for the updated upsert_debounce_key queries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(debounce): serialize upsert per key (simpler, race-free)

Companion to windmill-ee-private: the running-survivor guard and batch
chaining are now protected by a per-key advisory lock instead of
snapshot-sensitive single-statement SQL. This closes a concurrent-arrival
data-loss race where a debounced late arrival's args could be dropped
because the batch lookup couldn't see the predecessor's just-committed
batch row.

Extends test_debounce_concurrent_arrivals_after_running_survivor to pull the
survivor and assert its accumulation includes BOTH racing late arrivals
(shared batch), and refreshes the SQLx cache for the rewritten queries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(debounce): atomic upsert robust to concurrent pull-time key deletion

Companion to windmill-ee-private: keep upsert_debounce_key a single atomic
INSERT ... ON CONFLICT DO UPDATE so a chaining push cannot fail when the
worker pull path concurrently deletes the holder's debounce_key (the prior
read+UPDATE split could hit "no row updated"). Adds
test_debounce_push_races_key_deletion_by_pull (races a chaining push against
the key deletion 50x, asserts the push never errors) and refreshes the SQLx
cache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(debounce): claim-based exactly-once batch consumption

Eliminates the rare duplicate/loss when two survivors land on one debounce
batch (a narrow push/pull race), without locking the worker pull hot path.

- migration: v2_job_debounce_batch gains consumed_at + consumed_by.
- pull side (maybe_apply_debouncing): instead of deleting the batch on consume,
  a survivor atomically claims its own row + any unclaimed siblings (stamping
  consumed_by = itself) and accumulates exactly the rows it claimed. A second
  survivor of the same batch finds its row already consumed by another job and
  runs empty (no duplicate); a re-pulled survivor recognizes its own prior claim
  and keeps its accumulated args; a never-batched job (CE/legacy) keeps its own
  args. Non-accumulate debounce paths still hard-delete their batch rows.
- complete_debounced_job (EE companion) never completes a running predecessor,
  so its in-flight run is not killed (no loss); the claim then prevents the
  duplicate the guard would otherwise allow.
- monitor: GC sweep deletes consumed batch rows past a 1h grace.

Together with the running-survivor guard this makes debounce accumulation
exactly-once. Adds tests: batch_consumed_exactly_once, repull_keeps_accumulated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(debounce): exhaustive edge cases + tighten consumed-batch GC grace

Tighten the consumed debounce-batch GC grace 1h -> 10min: per-op cost of the
claim is unchanged (an indexed mark is as cheap as the old delete), so the only
cost of retaining consumed rows is table growth, which a shorter grace bounds
under high-throughput debounce (a survivor that could still reference a row is
pulled long before 10min; GC is not correctness-critical since a re-pull whose
row was swept falls back to its persisted args).

Adds edge-case tests: never-batched keeps own args (CE fallback), concurrent
claim partitions a batch disjointly (exactly-once under real concurrency),
three survivors -> first takes all / rest run empty, non-accumulate debounce
hard-deletes its batch rows (no leak), and the GC sweep deletes only
past-grace consumed rows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(debounce): port the #9781 regression case, flow-node guard, full-path bench

- Port the regression from #9781
  (test_post_preprocessing_debounce_into_running_survivor_loses_message):
  post-preprocessing survivor accumulates + runs, a later same-key message must
  start a new batch (survive) not be folded into the running survivor. Exercises
  the full EE path via jobs_ee::maybe_debounce_post_preprocessing.
- Add the third EE entry point's guard:
  test_flow_node_debounce_running_survivor_not_superseded (maybe_debounce_flow_node).
- Add an #[ignore] full-source throughput bench (bench_debounce_full_path) driving
  the real maybe_debounce + maybe_apply_debouncing end-to-end.

All debounce tests exercise the real jobs_ee implementation (run with
--features private,enterprise); none stub it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(debounce): scalar-arg accumulation + GC-then-repull no-loss

Close two accumulation edge gaps (both run on --features private,enterprise,
exercising the real jobs_ee path):
- accumulate bare-scalar values (the T | T[] union fallback): each scalar is
  wrapped and accumulated into the survivor's list.
- GC reclaiming a survivor's consumed batch row before a re-pull must not lose
  data: the re-pull finds no row and keeps its already-persisted accumulated
  args (had_row=false fallback), rather than running empty.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(debounce): real-worker end-to-end accumulation test

Drives the full real path on --features enterprise,deno_core,private: push 3
same-key debounced flow jobs (real push() -> maybe_debounce collapses the
batch), a real worker pulls the survivor (real pull() -> maybe_apply_debouncing
claim+accumulate) and executes the deno flow, then asserts the executed result
is the full accumulated set [1,2,3] and the two superseded messages are skipped.
Complements the in-process unit tests with a genuine worker-execution run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(debounce): atomic claim+persist, GC only non-queued rows; reword comment

Address review findings:

- [P1] Claim and accumulated-args persist are now in one transaction. Before,
  a crash between stamping batch rows consumed_by=self and the `UPDATE v2_job
  SET args` could let a zombie re-pull see its own prior claim and keep only its
  own args (dropping the siblings it had claimed). Wrapping claim + accumulate +
  persist in a tx makes them commit together or roll back together (re-pull then
  re-claims cleanly).

- [P1] GC of consumed batch rows now also requires the job to no longer be in
  v2_job_queue. A consumed sibling can stay queued well past any time grace under
  a concurrency limit / backlog; reclaiming its marker by age alone let its
  eventual pull treat it as never-batched and re-run its item (a duplicate).
  Keeping the row until the job leaves the queue preserves the "already consumed"
  signal. Test extended with a still-queued consumed row that must survive GC.

- [P2] Drop "Customer" attribution from a test doc comment (AGENTS.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(debounce): emit accumulation log after committing the claim transaction

append_logs opened a second pool connection while the claim transaction (and its
batch row locks) were still held; under concurrent debounced pulls that risks
pool-exhaustion stalls/timeouts. Defer the log line until after tx.commit().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to 6aabd7c5ce53b9153be05c3e7bc9a76eadb1a48a

This commit updates the EE repository reference after PR #631 was merged in windmill-ee-private.

Previous ee-repo-ref: 30d740e619fad219108ec4b4c6a9d67c1ab42d46

New ee-repo-ref: 6aabd7c5ce53b9153be05c3e7bc9a76eadb1a48a

Automated by sync-ee-ref workflow.

* fix(debounce): claim whole batch in one UPDATE (no deadlock); assert test setup

Both Codex (P1) and Claude (P2) flagged a deadlock: the claim used two writable
CTEs (claim_self then claim_rest), locking the self row before siblings, so two
survivors of the same batch pulled concurrently acquired row locks in opposite
order and PostgreSQL aborted one with deadlock_detected (a transient pull error
on exactly the two-survivors race this path handles).

Replace with a single `UPDATE ... WHERE debounce_batch = (...) AND consumed_at IS
NULL RETURNING id` that claims the whole batch: both transactions lock rows in
the same scan order, so one simply waits and re-evaluates under EvalPlanQual.
A `claimed_self` flag (EXISTS id = self in the claimed set) plus the `mine`
snapshot still distinguishes fresh-claim / consumed-by-other / own-re-pull.

Also assert add_survivor_to_batch_of actually inserts a row (rows_affected == 1)
so a mis-set-up test can't pass vacuously.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

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>
2026-06-25 16:36:16 +00:00
Ruben Fiszel 6664ce6dc0 fix(frontend): apply script editor timeout to preview/Test runs (#9794)
The custom timeout configured in the script editor settings was only
honored for deployed script runs: it is persisted on the script row and
passed as custom_timeout when running by hash/path. Preview ("Test")
runs derive their timeout solely from the `timeout` query param of
/jobs/run/preview, which the editor never sent, so Test silently fell
back to the instance default.

Forward the editor's timeout setting through ScriptBuilder ->
ScriptEditor -> JobLoader.runPreview as the preview run's timeout query
param. The backend already clamps custom_timeout against the instance
max in resolve_job_timeout, so previews get the same ceiling as deployed
runs.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:34:54 +00:00
centdix 9d61e4e59e feat: self-host docs search for chat, mcp, cli; drop inkeep (#9772)
* feat: self-host docs search for chat, mcp and cli; remove inkeep

Embed a vendored docs snapshot (llms.txt/llms-full.txt) in the backend and
serve ranking + page rendering from GET /api/docs/{search,page}. The AI chat,
the MCP searchDocs/readDocsPage tools, and 'wmill docs' all consume it, so docs
search works with no runtime egress and is no longer EE-gated. Removes the
inkeep proxy. EE companion deletes inkeep_ee.rs (ee-repo-ref bumped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: name read_docs_page param `url` instead of `path`

search_docs returns each hit's `Source` URL, so the read tool now takes a
`url` argument to match — the AI/MCP loop reads "search gives a Source URL,
read takes that url" rather than copying a `Source:` URL into a `path` slot.
A bare `/docs/...` path is still accepted and canonicalized before lookup.

Regenerated openapi-deref, the MCP endpoint tools, and the frontend client.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci: add scheduled workflow to refresh the vendored docs snapshot

The backend embeds docs_snapshot/*.gz at build time, so the in-product docs
corpus is otherwise only as fresh as the last manual fetch.sh run. This adds a
weekly (and manually dispatchable) job that re-runs fetch.sh, sanity-checks the
result against truncation/garbage, and opens a PR via the internal app when the
snapshot changed — so a human reviews the docs diff before it rides into the
next release build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor: make docs tool-result strings caller-neutral

The search/page endpoints back three differently-named consumers (the AI chat
`read_docs_page` tool, the MCP `readDocsPage` tool, and the `wmill docs` CLI),
so the shared rendered text shouldn't name one of them. Refer to "the docs
page-reading tool" and its `url` argument instead, and add tests pinning the
caller-neutral follow-up guidance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: point ee-repo-ref at inkeep-removal companion rebased on EE main

The companion branch now carries only the inkeep_ee.rs deletion on top of EE
main (was based on the native-job-retry EE line, which polluted the EE PR diff).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(docs): expose docs:read in token catalog; precompute lowercased corpus

Addresses two review nits on the self-hosted docs PR:

- docs:read was enforced (ScopeDomain::Docs) but missing from the token scope
  catalog (token.rs ALL_SCOPES), so it couldn't be selected when creating a
  standard scoped token in the UI — leaving scope-restricted CLI/MCP docs use
  effectively ungrantable. Add a read-only "Documentation" group (no write
  surface) and a test asserting it is exposed.
- search ran page.body.to_lowercase() on the whole corpus per query. Lowercase
  body/title/description once at parse time (into the OnceLock corpus) and scan
  the precomputed copies instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: update ee-repo-ref to 27a4f41b8e5603d6e444efcfc420bd1c44a07eed

This commit updates the EE repository reference after PR #630 was merged in windmill-ee-private.

Previous ee-repo-ref: c7ec3a0c2fa38d4cb5e50bf0265eef4710de4860

New ee-repo-ref: 27a4f41b8e5603d6e444efcfc420bd1c44a07eed

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>
2026-06-25 16:32:07 +02:00
Ruben Fiszel 11d83ab1ec fix(python): serialize concurrent installs into shared wheel cache dir (#9787)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:08:40 +00:00
Ruben Fiszel 6c71c33470 fix(python): re-verify wheel RECORD on local cache reuse (once per worker) (#9775)
A corrupt local pip cache entry (e.g. wmill==1.739.0 missing s3_reader.py
after out-of-band file loss on a persistent/shared cache volume) was trusted
indefinitely: handle_python_reqs only checked the .valid.windmill marker on
the reuse fast path. verify_wheel_record already guarded the install and
S3-pull paths, but never ran again once the marker existed.

Re-verify the wheel RECORD on the first reuse of each cache entry per worker
process and repair (wipe + reinstall) on failure. A VERIFIED_VENVS in-memory
set makes every subsequent reuse skip the scan, so the warm-cache hot path
keeps paying only its original single stat.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:58:49 +00:00
Ruben Fiszel aa098c70c0 perf: drop v2_job side-table ON DELETE CASCADE FKs to speed retention deletes (#9786)
* perf: drop v2_job side-table ON DELETE CASCADE FKs to speed retention deletes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: document delete_jobs auth contract and workspace-scope jobs_export purge

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:52:30 +00:00
Ruben Fiszel 962758c02d fix: pass SSL cert env vars to uv python install (#9790)
`install_python` cleared the subprocess environment via `env_clear()` and
forwarded only a subset of variables, omitting `SSL_CERT_FILE` (from
`PY_INDEX_CERT`/`PIP_INDEX_CERT`) and `UV_NATIVE_TLS` (from `PY_NATIVE_CERT`).
This caused `invalid peer certificate: UnknownIssuer` errors when downloading
managed Python runtimes in environments with corporate/private CAs.

Forward both variables, mirroring the sibling `find_python` method and the
pip install path in `python_executor.rs`.

Fixes WIN-2100

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:35:58 +00:00
Ruben Fiszel 754cae956a fix: use transaction for parallel_monitor_lock DELETE in last-iteration path (#9789)
In the last-iteration path of a parallel for-loop (nindex == len), the
DELETE FROM parallel_monitor_lock ran on the pool (db) while the
transaction tx (begun earlier) was still held. This dual-connection
pattern requires 2 simultaneous connections from the per-worker pool
(default max 5) and can trigger "pool timed out while waiting for an
open connection" under concurrent load.

Run the DELETE on the held transaction (&mut *tx) instead, matching the
fix PR #7861 applied to other queries in this file. The transaction is
committed shortly after, so including the DELETE in it is safe and
consistent with the non-last-iteration path.

Fixes WIN-2099

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:34:05 +00:00
Ruben Fiszel 12f92e3ab7 [ee] feat(backend): native script retry without one-step-flow wrapping (#9688)
* feat(backend): native script retry without one-step-flow wrapping

Schedules and data pipelines that retry a single script previously wrapped
it in a one-step flow (JobKind::SingleStepFlow), creating extra job rows, a
v2_job_status row, and UI projection complexity. This adds native retry on a
plain JobKind::Script job.

- RetrySettings: flatten Retry into a deduped retry_settings table, carried
  via the existing runnable_settings_handle (lazy, off the hot path).
- push() materializes a bare-script-with-retry SingleStepFlow into a native
  Script job (gated on min-version + no handlers/retry_if).
- add_completed_job re-pushes the next attempt on failure with backoff,
  tracking the attempt counter in v2_job_queue.extras and the chain via
  parent_job; schedule completion handlers fire only on the terminal attempt.
- frontend: ScriptRetryChain shows the attempt chain on the run page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(backend): native retry_if eval + per-occurrence schedule handlers

Extends native script retry to the two cases that previously stayed on the
one-step-flow path:

- retry_if: evaluated natively on the failure path via a feature-gated
  windmill-jseval dep (quickjs) over the failure result + flow_input; push
  materializes such policies natively only when quickjs is available.
- on_failure_times / on_recovery: apply_schedule_handlers now resolves each
  past scheduled occurrence's terminal status across its native-retry chain
  (root OR any parent_job=root child succeeded) and excludes the current
  occurrence, so the counting is per-occurrence rather than per-attempt.

All scheduled-script retries now go native (schedule.rs gate removed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(backend): always materialize retry_if natively; unsupported without quickjs

retry_if is evaluated by the worker (which always has quickjs), not the
pusher, so gating materialization on the pusher's feature was wrong. The
flow path was never a real fallback either — the flow runtime needs quickjs
to evaluate retry_if too. retry_if now always goes native; on a worker
without quickjs it is unsupported and fails closed (no retry).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(backend): un-park asset-cascade (pipeline) retry

Native retry resolves the blocker that parked pipeline retry: a retried
subscriber is now a Script job (not a one-step flow / flow step), so it
stays eligible for asset dispatch and can trigger its own downstream on
recovery.

- scripts.rs: persist // retry <count> [<delay>] to script_trigger on asset
  edges (was dropped with a TODO warning).
- asset_dispatch.rs: is_eligible_kind keys off flow_step_id, not parent_job,
  so native-retry attempts dispatch on success while flow steps stay excluded.
- tests: retry-bearing subscriber now dispatches as a native Script carrying
  the policy in runnable_settings_handle; native-retry attempt is eligible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(backend): cap native retry interval, lazy result serialization, idempotent retry push

Hardening from a self-review of the native retry path:
- Cap the backoff at MAX_RETRY_INTERVAL to match the flow-runtime path
  (evaluate_retry); the exponential formula could otherwise schedule up to
  ~18h vs the flow path's 6h.
- Serialize the failure result lazily (only when a retry_if policy needs it),
  so the common failure no longer pays the serialization on the failure path.
- Push each retry with a deterministic id per (root, attempt). If a worker
  dies between enqueueing the retry and finalizing the current attempt, the
  reaper re-handles the attempt and lands here again — push rejects the
  duplicate id, so the retry is enqueued exactly once (no double-retry).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(backend): defer schedule handlers idempotently on retry-push replay (review P1)

Address local-review findings:
- P1: retry_pending was derived from the retry push *result*, so on a worker
  crash + reaper replay the duplicate-id push returned Err → retry_pending
  flipped to false → apply_schedule_handlers fired for the non-terminal
  attempt (and the terminal attempt later fired them again). Pre-check whether
  the deterministic retry id already exists and report it as pending without
  re-pushing, so the handler-deferral invariant is crash-idempotent too.
- P2: refresh the stale 'wrap the script in a one-step flow' comment in the
  asset-cascade retry push — it now materializes a native Script.
- Add RetrySettings <-> Retry round-trip unit tests (clamping edges).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(backend): native retry chain + per-occurrence status sqlx tests

Close the two integration-test gaps flagged in local review:
- chains_attempts_and_is_idempotent: drives maybe_enqueue_native_script_retry
  through attempt0 -> retry1 -> retry2 -> exhausted (counter, backoff, max-attempts)
  and asserts crash-replay idempotency (the P1 fix: a replayed completion reports
  pending without double-enqueueing).
- per_occurrence_status_counts_recovered_as_success: pins the exact per-occurrence
  terminal-status query from jobs_ee::apply_schedule_handlers — a retried-but-
  recovered occurrence counts as success, retries (parent_job set) are excluded
  from occurrence counting, and the current occurrence is excluded.
- canceled_job_does_not_retry: cancellation wins over a pending retry.

Runtime sqlx API (no .sqlx cache entry needed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): exclude schedule handlers from the retry-attempt chain

The retry chain listed all script children of the root by parent_job, but
schedule completion handlers (on_failure/on_recovery/on_success) are also
script children — when the occurrence has no retries, the handler's parent is
the root itself, so a successful, never-retried job rendered a bogus
'Retries (1)' badge pointing at the handler. Filter children to re-runs of the
same script (matching script_hash); real retries keep the root's hash, handlers
run a different script.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(frontend): surface schedule handlers on the run page

Extend the run-page chain component with schedule completion handlers:
- A 'Handlers' row on a scheduled job links to the on_failure/on_recovery/
  on_success runs that fired for that occurrence (found as children of the
  terminal attempt, identified by their synthetic created_by).
- A handler's own run page now shows a 'Failure/Recovery/Success handler'
  label with a link back to the run it handled and its schedule. on_recovery
  and on_success share created_by, disambiguated by the recovery-only
  error_started_at arg.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(backend): restore folder_default_permissioned_as sqlx caches dropped by prepare

An earlier `cargo sqlx prepare` on this branch ran before #8801's
folder_default_permissioned_as test merged in, so it pruned the 3 query caches
that test needs; cargo_test then failed under SQLX_OFFLINE. Restore them from main.

* fix(backend): only cascade assets from native retry attempts, not handlers (review P1)

is_eligible_kind keyed dispatch on flow_step_id alone, so every parented Script
child became asset-eligible — including schedule/error/recovery handlers (Script
jobs with parent_job set and no flow_step_id). A handler that declares assets
would then trigger a cascade the old parent_job IS NULL guard prevented. Gate
parented jobs on being a genuine retry attempt: a re-run of the SAME runnable as
its chain parent (handlers run a different script). Runtime query, no sqlx cache.

* fix(backend): cache the private-gated retry_setting asset-dispatch test query

The same prepare-without-private that dropped the folder_default caches also
pruned the cache for the retry_setting_dispatches_subscriber_as_native_script
test query (asset_trigger_dispatch.rs:721). Regenerated with --features private.

* fix(backend): exclude handler children from per-occurrence recovery (review)

A scheduled occurrence's on_failure/on_success handler runs as a successful
child (parent_job = occurrence), and the per-occurrence success EXISTS counted
ANY successful child — so a failed occurrence whose error handler succeeded was
marked 'recovered', breaking on_recovery (test_script/flow_schedule_handlers in
the merge) and on_failure_times counting. EE query now scopes the EXISTS to
same-runnable children (only native retry attempts); regenerate sqlx cache + bump
ee-repo-ref. native_retry_test gains a handler-child regression case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(backend): scheduled-script retry is a native Script, not SingleStepFlow

test_push_script_with_retry / test_try_schedule_with_retry (from main) asserted
the old SingleStepFlow wrapping for scheduled-script retry; this PR makes it a
native Script. Update both to assert kind='script' and that the retry policy is
carried via runnable_settings_handle.

* fix(backend): preserve dedicated_worker on native retry + saturate count casts (cubic)

Address cubic CI review:
- P1: the SingleStepFlow->native Script materialization dropped dedicated_worker,
  so a dedicated-worker scheduled script lost its dedicated pool on retry. Resolve
  it from the script row in push so the materialized Script keeps the dedicated tag.
- P2: saturate the u32->i32 retry-attempt narrowings (RetrySettings::from) and the
  u32->i16 // retry count narrowing (scripts.rs) instead of wrapping.

* fix(backend): use a retry-specific signal, not runnable equality (codex review)

Address Codex CI review:
- P1: is_native_retry_attempt treated any same-runnable parented Script child as
  a retry. WAC v2 inline children have that exact shape, so an inline child of an
  asset producer would cascade. Use a retry-specific signal instead: the job
  carries a retry_settings policy (always re-inserted by maybe_enqueue) and has no
  flow_innermost_root_job. Apply the same flow_innermost guard to the EE
  per-occurrence EXISTS (WAC inline children must not count as a recovery).
- P1: the deterministic retry-id pre-check raced with push; a concurrent duplicate
  now resolves as 'retry pending' (re-check on the duplicate-id error) instead of
  flipping retry_pending to false and firing handlers early.
- Tests: native_retry + asset_trigger_dispatch gain WAC-inline-child cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(backend): explicit native_retry_attempt marker, drop heuristics

Replace the per-site "is this a retry?" inference (parent_job + runnable match +
flow_innermost / retry_settings) with one explicit marker: a sparse
native_retry_attempt(job_id, attempt) table, written in maybe_enqueue. The marker
also carries the attempt counter (previously in v2_job_queue.extras), so it's the
single source of truth.

- asset_dispatch: is_native_retry_attempt is now one indexed EXISTS on the marker.
- EE per-occurrence query: joins the marker instead of guessing by runnable/flow_innermost.
- maybe_enqueue: reads/writes the marker (persistent) instead of queue extras.
- Lifecycle: swept with the job in retention (log_cleanup), no FK to keep bulk delete cheap.
- Eliminates handler / WAC-inline-child misclassification by construction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(backend): sweep native_retry_attempt markers in the periodic retention path too (codex)

The marker has no FK and relies on retention cleanup; log_cleanup.rs swept it but
the periodic monitor.rs path deleted v2_job rows without it, orphaning markers.
Add the same WHERE job_id = ANY(...) sweep there.

* fix(backend): widen native_retry_attempt.attempt to integer (cubic)

The smallint column was cast to/from u32 and could wrap a retry chain longer than
i16::MAX into premature exhaustion. Use integer, matching the retry policy's i32
attempt count, so no narrowing occurs on the maybe_enqueue read/write path.

* feat(frontend): mark retries via is_retry on listJobs; drop SAVEPOINT

- Expose an is_retry flag on jobs (UnifiedJob/CompletedJob/QueuedJob + openapi),
  computed from the native_retry_attempt marker. The run-page chain now filters
  retry attempts by is_retry instead of the script_hash heuristic, so WAC v2
  inline children (same script, parent_job) no longer render as retries (codex).
- Revert the marker-cleanup SAVEPOINT (an unused pattern in this codebase): keep
  the plain catch-and-continue matching the other side-table deletes; the table is
  created by a startup migration so it always exists when cleanup runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(backend): mark is_retry sqlx(default) so non-list job queries can omit it

The single-job GET query maps directly to CompletedJob/QueuedJob via FromRow but
does not select is_retry, which errored with "no column found". Only the list
endpoint populates the marker; #[sqlx(default)] lets every other query omit the
column and default to None.

* feat(backend): select is_retry in single-job GET too for consistency

The list endpoint already exposes the marker; populate it on the single-job GET
(both completed and queued variants) as well so a run loaded directly reflects
its retry status. #[sqlx(default)] stays as a safety net for any other query.

* feat(backend): reap orphaned native_retry_attempt markers via periodic sweep

The marker has no FK to v2_job (to keep the hot bulk retention delete cheap), so
direct job deletions (workspace/job delete, schedule clearing) would leave marker
rows orphaned. Rather than add explicit cleanup to every v2_job delete site (which
must then be remembered for every future path), reap orphans in the periodic
delete_expired_items pass: DELETE FROM native_retry_attempt WHERE NOT EXISTS (the
job). The table is sparse so the anti-join drives off it and probes v2_job by PK —
cheap. Retention still sweeps markers inline (keeps the table small so this stays
cheap); a transient orphan is harmless (nothing reads is_retry for a gone job).

* fix(frontend): include flow handlers in retry chain handler row (codex)

Schedule on_failure/on_recovery/on_success handlers can be flow paths (flow/...),
whose handler job is a flow, not a script. The chain fetched children with
jobKinds:'script', hiding flow handlers. Drop the kind filter — retry attempts
are still selected by is_retry and handlers by created_by, so both kinds surface.

* fix(backend): carry concurrency/debouncing settings into native retries

maybe_enqueue re-pushed the next attempt with ConcurrencySettings/DebouncingSettings
::default(), dropping the script/pipeline concurrency settings the failed job carried
in its runnable_settings_handle. A retry of a concurrency-limited script then inserted
no concurrency_key and ran unbounded. Resolve both from the same handle (cached) and
pass them in the payload, which push forwards to the materialized retry. Adds a
regression test asserting the retry's handle resolves to the concurrency settings.

* fix(backend): carry concurrency/debounce into scheduled-retry root + document retry-helper auth (codex)

P1a (schedule.rs): the scheduled-retry materialization fetched the script's
concurrency/debounce settings but passed ConcurrencySettings/DebouncingSettings
::default() into the SingleStepFlow payload, so the root attempt's handle held only
the retry policy and the whole chain ran unbounded. Pass the fetched settings.
Regression test asserts the root handle resolves to retry + concurrency.

P1b (jobs.rs): document maybe_enqueue_native_script_retry's authorization contract
— it is pub only for the integration test; the sole production caller is the worker
completion path passing a DB-derived, already-authorized MiniCompletedJob.

* docs(backend): attach native-retry auth contract to the function itself (codex)

The doc block was merged with eval_retry_if's doc and bound to that function,
leaving maybe_enqueue_native_script_retry undocumented. Split them: eval_retry_if
keeps its own doc; the native-retry + authorization contract now sits directly
above maybe_enqueue_native_script_retry.

* docs(backend): regenerate served openapi-deref with is_retry + fix stale comments (codex)

- Regenerate openapi-deref.{yaml,json} (served from lib.rs): they were stale since
  1.734.0 and lacked is_retry on QueuedJob/CompletedJob, so clients reading the
  served spec couldn't see the field. Now current at 1.739.0.
- schedule.rs: a retry_if gate is evaluated at failure time and fails closed without
  quickjs (no retry); it does not fall back to a flow path.
- windmill-types jobs.rs: is_retry is selected by both the list and single-job GET
  endpoints (not list-only).

* docs(backend): fix remaining stale retry_if/quickjs comments (codex)

The retry_if block and the push materialization comments claimed push keeps
retry_if on a flow path / the worker always has quickjs. The code always
materializes native retry and the no-quickjs eval_retry_if path fails closed —
correct the comments to that constraint.

* docs(backend): fix stale quickjs-fallback + schedule-handler-restriction comments (codex)

- Cargo.toml quickjs feature: without quickjs a retry_if gate cannot be evaluated
  and the job does not retry (no one-step-flow fallback).
- jobs.rs handler-defer comment: apply_schedule_handlers resolves per-occurrence
  failure/recovery status across the retry chain, so the old 'restricted to
  schedules whose handlers don't need per-occurrence counting' claim is dropped.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:22:26 +00:00
Ruben Fiszel cd42c6ca18 fix: decrypt secret variables via external backend in common resolvers (#9784)
`get_variable_or_self`, `get_variable_or_self_as`, `get_secret_value_as_admin`
(and `transform_json_unchecked`'s `$var:` branch) in windmill-common always ran
the raw `variable.value` through `decrypt()`. With an external secret backend
(HashiCorp Vault / Azure Key Vault / AWS Secrets Manager) configured, that
column holds a `$vault:`/`$azure_kv:`/`$aws_sm:` marker rather than base64
ciphertext, so base64 decoding failed with `Invalid byte 36, offset 0` (the
`$`). This broke GitHub App git sync (git_sync_ee.rs) and any other consumer of
these resolvers when an external backend is active.

Move backend resolution (`get_secret_backend`, `get_secret_value`,
`is_*_stored_value`, caching) into `windmill-common::secret_backend::resolver`
so the low-level variable resolvers can route external markers through the
configured backend's `get_secret()` instead of `decrypt()`. The windmill-store
and windmill-api `secret_backend_ext` modules now re-export these from
windmill-common (single source of truth / single backend cache) and keep only
their write-side helpers. No `_ee.rs` files change.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 12:05:21 +02:00
Diego Imbert 170cd79aaf fix: allow hyphens in postgresql database name validation (#9782)
* fix: allow hyphens in postgresql database name validation

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: cover hyphen acceptance in validate_dbname

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 12:02:35 +02:00
Ruben Fiszel f6998ec54c feat: data tests for ducklake pipeline materialization (#9708)
* feat: data tests for ducklake pipeline materialization

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(frontend): data_test count badge on pipeline graph nodes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: surface annotation badges (incl. data_test) on deployed pipeline nodes

Backend graph endpoint now parses each pipeline member's deployed body and returns partition/freshness/tag/retry/data_test, so badges render on deployed nodes, not only live drafts. Aligns the TS DataTest.relationships fields to snake_case to match the Rust serde wire shape (the type is now populated from both the parser and the backend JSON).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): keep materialize output edge when editing the producer in the pipeline graph

The live-edit overlay re-derived a selected/edited script's lineage from // on inputs + body-inferred assets only, so the // materialize <asset> output (an annotation, not body SQL) was judged stale and its write-edge dropped on select — leaving the materialized asset unlinked (and the node's annotation badges hidden). Include the parsed materialize target in liveRefKeys and the draft writeOuts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: run all data tests in one pass with a structured per-test result

Replace the raise-on-first-violation probes with a single materialize summary that embeds every test's violating-row count in a data_tests column (computed in a CTE, since DuckDB rejects subqueries inside struct literals). The worker reads the breakdown and decides pass/fail: a clean run returns the per-test summary in the result; a failing run errors with the FULL list (every test, ✓/✗ + counts), not just the first failure. Verified live (EE) for built-ins + custom, pass and multi-failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(frontend): data-test pass/fail checklist in the job result

DisplayResult renders a per-test checklist (✓/✗ + violation counts) above the raw result for managed materialize runs — from the structured data_tests on success, and parsed from the worker's breakdown message on failure. Shows in the script editor Test panel, the runs page, and the pipeline asset run pane.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(frontend): move data-test badge onto the producer→asset edge with run status

The test badge now sits on the write-edge (the transformation link) rather than the producer node, since the tests assert on what the transformation produces. It's tinted by the producer's last-run status (green = passed, red = a test failed) and its hover title lists every declared test. Removes the now-redundant node badge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(frontend): render custom data-test scripts as their own clickable graph nodes

A // data_test <script_path> custom test now appears as its own node below the asset it validates, joined by a dashed 'tests' edge. Clicking it opens the test script in the detail pane (dispatched like any runnable). Built-in tests stay folded into the edge badge; only script-backed tests become nodes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): type data-test edge field via AssetGraphResponse, not in-scope g

BuiltEdge is declared at component scope, outside build(g), so referencing typeof g.runnables in its type failed CI's svelte-check (Cannot find name 'g'). Use the imported AssetGraphResponse type instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): anchor edge badge on routed path + a11y text on test icons

Address review: the data-test edge badge anchored on the straight-line midpoint, floating off detoured edges — anchor it at detourX when the edge is routed through a gutter lane. Add sr-only pass/fail text so the checklist icons are distinguishable to screen readers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: close data-test enforcement bypass + gate badges to scripts + reject multi-stmt custom tests

Address review (cubic) findings:
- P1: managed materialize generates its own summary row carrying data_tests, and enforcement reads that column — but a // result_collection annotation (e.g. a scalar mode) could reshape the row and drop data_tests, silently bypassing a failing test. Force LastStatementAllRows for managed materialize runs so the summary row is always intact.
- P2: asset-graph annotation badges were keyed by path only, so a flow sharing a path with a pipeline script inherited its badges. Gate the lookup on usage_kind == Script.
- P2: a custom test body is embedded as a subquery, so a multi-statement body produced invalid SQL with an opaque DuckDB error. Validate single-statement up front with an actionable error; align docs/comments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: fail loud if fewer data-test outcomes recovered than declared

Defense-in-depth from the fresh-context review: enforcement reads per-test outcomes off the materialize summary row, but if the data_tests column were ever dropped/reshaped at the FFI boundary, extract_data_tests would return fewer (or zero) outcomes and the run would silently pass unverified tests. Track the embedded test count on MaterializeExec and abort with a clear error when recovered < declared. Verified: normal run (4==4) unaffected; the scalar-result_collection bypass already fails.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: relationships data test same-lake reuse + schema-qualified target quoting

Address Codex/Pi review (two P1s in the relationships codegen):
- A relationship into the same ducklake as the materialize target minted a second ATTACH of that lake under _wm_ref_N while _wm_target already held it — DuckDB forbids attaching one database twice, so the test failed before it could run. Reuse _wm_target for same-lake references.
- A schema-qualified target (ducklake://warehouse/main.dim_products.sku) emitted FROM _wm_ref_0."main.dim_products" — one quoted identifier with a literal dot — silently querying a nonexistent table. Quote each dotted segment so the dot stays a schema separator.
Adds tests for both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): refresh data_test badge on deployed-script drafts + scope to materialize target

Address Codex review nits (both P2):
- resolveGraph: the existing-runnable draft-overlay branch kept the deployed data_tests, so adding/removing // data_test lines on an already-deployed script left the badge stale until redeploy. Refresh it from the live parse like the new-runnable branch.
- AssetGraphCanvas: data tests were attached to every write-edge from a producer. They assert on the // materialize target (always a ducklake asset in v1), so only the ducklake write-edge now carries the badge and custom-test nodes — a producer's other (S3/datatable) outputs no longer show them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:47:02 +02:00
hugocasa 88fca6a8c1 fix: enforce containment of python module dir for preview jobs (#9704)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:13:59 +02:00
Ruben Fiszel d131d754e1 feat: ducklake time-travel UX (snapshot history + AT VERSION reads) (#9709)
* feat: ducklake time-travel UX (snapshot history + AT VERSION reads)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: catalog-qualify ducklake time-travel FROM hints (lake. prefix)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: render ducklake snapshot_time (microseconds since epoch) correctly

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: merge ducklake History + Query into one master-detail tab

Snapshot list (left) selects the version previewed in the read-only grid (right); newest auto-selected. Copy-clause moved to the preview's SQL line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: scope ducklake snapshot history to the table's versions

Catalog-wide snapshots predate a table's creation; previewing AT a version before the table existed errored ("Table ... does not exist at version N"). The DUCKLAKE_SNAPSHOTS marker now takes the table and lists only snapshots from its first creation onward. Also: narrower snapshot-list pane on large screens (target a fixed width, not a fixed fraction).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: load ducklake preview columns at the pinned version + reset on asset switch

Addresses CI review (codex/pi P1, cubic P2):
- Historical previews loaded current-schema columns, so an AT(VERSION) read enumerating a column added in a later snapshot failed. Now DESCRIBE-loads the column set at the pinned version; the read is gated on columns matching the current version to avoid a stale-colDefs race on version switch.
- selectedVersion no longer sticks across assets: the panel is keyed on path (remounts per asset) and effectiveVersion falls back to newest when the pick isn't in the current list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: match History tab UI (master-detail, full-FROM copy) after merge

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: handle catalog-only ducklake asset paths (no table segment)

parseDbInputFromAssetSyntax threw on a catalog-only path like 'ducklake://main' (undefined.split('.')) — a real graph node (e.g. a consumer of the whole catalog). It now returns a table-less input instead of throwing, and DucklakeAssetPanel renders only the partition grid (no per-table history/time-travel) for table-less nodes. Adds parser unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: escape ducklake catalog name in client-built time-travel DESCRIBE

fetchDucklakeColumnsAtVersion interpolated the catalog name into an ATTACH string literal without escaping; double single-quotes (mirrors backend escape_sql_literal) so a quote-containing catalog name can't break out. Also fixed the v1.x docs checklist line to match the shipped full-FROM copy affordance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:11:47 +02:00
Ruben Fiszel 920f5688ca chore(main): release 1.739.0 (#9746)
* chore(main): release 1.739.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-06-24 18:01:19 +00:00
hugocasa b5bd8245d8 fix: reject symlink traversal in job-dir path validation (#9713)
* fix: reject symlink traversal in job-dir path validation

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: cover dangling symlink in job-dir path validation

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: close symlink-traversal bypass via in-bounds `..` in path check

Walk the normalized relative path instead of raw user components, so an
in-bounds `..` (e.g. `foo/../evil/payload`) can no longer drift the walk
past a planted symlink. Adds regression coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:08:45 +00:00
Guilhem 42c5e7a3fc feat: scope AI sessions per workspace root with lifecycle reconcile (#9734)
* feat: scope AI sessions per workspace family with lifecycle reconcile

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: centralize session reconcile trigger + extract pure lifecycle decision

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf: remove unused workspace family index

* refactor: scope sessions by workspace root id, drop family_id column

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(sessions): preserve user-archived sessions when archiving their workspace

archiveSessionsForWorkspace tagged every session archivedByWorkspace, including ones the user had already archived by hand, so a later workspace unarchive auto-restored them. Skip already-archived sessions so only workspace-archived ones are tagged, matching decideSessionLifecycle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: archived-session banner with unarchive, suppress workspace-gone banner while archived

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: re-root sub-fork sessions on reconcile when an ancestor is deleted

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: group AI sessions by workspace family with show-all-workspaces filter

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: revert unrelated AIProviderPicker cosmetic changes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: hide per-session unarchive when workspace is gone, show move/discard instead

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: GC attached files on lifecycle delete + reconcile on sidebar fork delete

Addresses Codex review: deleteSessionsForWorkspace/reconcile delete now GC linked files (deleteItemsForSession), matching deleteSession; sidebar deleteFork now reconciles so surviving child forks re-root off the deleted ancestor. Also de-flaked post-rehydrate reads in the IndexedDB tests via vi.waitFor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: don't strand user if post-delete reconcile throws; refresh stale warmSessions comment

Addresses auto-review P2s: wrap reconcileAfterWorkspaceChange in deleteFork so the parent switch + navigation always runs even on reconcile failure; correct the warmSessions comment which no longer holds under 'Show all workspaces'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: don't fail/strand fork archive+delete when client session cleanup throws

Addresses cubic P1/P2 on forks/compare: the workspace archive/delete is authoritative; wrap the best-effort session cleanup + reconcile so a local IndexedDB failure neither falsely reports failure nor blocks navigation away from the gone fork. Mirrors the SidebarContent fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: drop drafting-history aside from reconcileAfterWorkspaceChange comment

Addresses auto-review P2: keep the refresh-before-reconcile invariant, drop the 'which they did inconsistently' narration per AGENTS.md (comments record constraints, not drafting history).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: clean up sessions on fork-id reuse + make all workspace-mutation cleanup best-effort

Addresses Codex P1s: (1) CreateWorkspaceInner 'permanently delete existing fork' (id-reuse) now drops local sessions for that id so they don't resurface on the recreated fork; (2) workspace_settings archive/delete and SidebarContent child-delete loop + main delete now treat post-mutation session cleanup as best-effort, so a local IndexedDB failure can't strand the user or abort remaining deletes (matching the compare-page fix).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: make fork-reuse session cleanup fire-and-forget (non-blocking)

Addresses cubic P2: don't await the best-effort cleanup so a slow IndexedDB op can't block the delete/reuse flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: drop previous user's transient drafts on user change

Addresses Pi P1: hydrateSessions preserved transient (unsent) drafts across user changes, so user A's draft + its pending fork/workspace state bled into user B's list and got reused by createSession. onUserChange now drops transients when the email changes; reconcile (intra-user) still preserves them. Regression test added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:41:18 +02:00
Ruben Fiszel 288318ac26 fix(apps): realign legacy raw-app drafts to raw_app draft kind (#9761)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:14:52 +02:00
Ruben Fiszel f5828780fd fix(backend): resolve folder_labels search_path on non-public (PG_SCHEMA) schemas (#9758)
* fix(backend): strip search_path=public from folder_labels migrations for non-public schema

The folder-labels migrations (20260610151334_folder_labels,
20260614075900_dedup_folder_labels) define `folder_labels(...)` with
`SET search_path = public` in their `CREATE FUNCTION` bodies. When Windmill
runs in a non-public schema (PG_SCHEMA), PostgreSQL validates the function
body against the `public` schema, where the `folder` table lacks the new
`labels` column, failing with `column "labels" does not exist`.

Add both migrations to OVERRIDDEN_MIGRATIONS, stripping the
`SET search_path = public` clause so the function inherits the current
search_path (which resolves the correct schema). Same regression and fix
pattern as PR #5400.

Fixes WIN-2093

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(backend): pin folder_labels search_path FROM CURRENT instead of stripping it

Keep the SECURITY DEFINER injection hardening while resolving the correct
schema on non-public (PG_SCHEMA) installs: FROM CURRENT snapshots the
migration connection's search_path at function creation time (public on
normal installs, the custom schema otherwise) instead of dropping the pin
and inheriting the caller's search_path at call time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(backend): repair migration to re-pin folder_labels search_path on applied instances

Instances that already applied the folder-labels migrations with the hardcoded
SET search_path = public have a folder_labels function pinned to public. On a
non-public (PG_SCHEMA) schema that reads the wrong folder table at runtime; the
OVERRIDDEN_MIGRATIONS fix only helps instances that have not applied them yet.

Add a CREATE OR REPLACE ... SET search_path FROM CURRENT migration that re-pins
the function to the migration connection's schema. No-op on public installs
(re-pins to public) and idempotent on already-correct ones.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:38:44 +00:00
Ruben Fiszel 8912e21d15 perf(monitor): vacuum job_perms/job_result_stream right after each orphan sweep (#9753)
A customer's top-load query was the job_perms orphan sweep (cleanup_job_perms_orphaned:
6.9s mean, 41s max). The cost is discovery, not deletion (~2.1ms per row deleted): the
NOT EXISTS anti-join seq-scans the whole job_perms heap to find a few orphans, and that
scan tracks the heap's physical size. job_perms / job_result_stream_v2 get one row per job
and are drained only by these per-cycle sweeps, so they churn hard — but the bulk
vacuuming_tables() runs only ~hourly, so dead tuples bloat the heap between bulk vacuums.

Reclaim right after each sweep instead: VACUUM (SKIP_LOCKED) the swept table when it
deleted rows. Plain VACUUM (not FULL) takes only SHARE UPDATE EXCLUSIVE so concurrent job
creates/reads proceed; the visibility map skips unchanged pages so repeated runs are cheap;
SKIP_LOCKED means HA replicas don't pile up (one vacuums, the rest skip). Benchmarked ~7x:
a bloated 268MB job_perms heap swept in 35ms vs 5ms vacuumed. Chosen over an autovacuum
reloptions migration so the behavior is explicit and lives with the sweep it pairs with.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:20:53 +02:00
Ruben Fiszel 55bed4abcf perf(audit): adaptive timestamp floor for S3 audit-log export (#9752)
* perf(audit): adaptive timestamp floor for S3 audit-log export (ee)

EE change in windmill-ee-private (src/ee.rs); this OSS commit carries the regenerated
sqlx cache for the new oldest-in-flight query and bumps ee-repo-ref.txt to the EE branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to ed89574be9117cda5e2d7d9de02cb5db066e93e3

This commit updates the EE repository reference after PR #628 was merged in windmill-ee-private.

Previous ee-repo-ref: 8a7f645c0a194a284fe19dd20dbe79dd0733dfdb

New ee-repo-ref: ed89574be9117cda5e2d7d9de02cb5db066e93e3

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>
2026-06-24 09:49:22 +02:00
hugocasa 043c2c05b7 fix: forbid superadmin job tokens from global user and token management (#9715)
* fix: forbid superadmin job tokens from global user and token management

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: extend superadmin job token guard to offboard and export routes

Apply forbid_superadmin_job_token to offboard_global_user and
export_global_users, the remaining global user-management routes that
were gated only by require_super_admin. Offboarding can delete a user
along with their tokens, password, invites and instance-group
membership, and export returns every user's password_hash, so both must
be unreachable by a superadmin job token.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 00:38:27 +02:00
Ruben Fiszel 9e4cf139b1 chore(main): release 1.738.0 (#9735)
* chore(main): release 1.738.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-06-23 21:08:15 +00:00
hugocasa cbf54d4eb4 fix: preserve fork parent linkage on workspace id change (#9716)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:54:07 +00:00
hugocasa 9793d01575 feat: add resource and infrastructure telemetry (#9737)
* 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>
2026-06-23 20:53:12 +00:00
hugocasa 24446e8009 fix: allow object storage test for non-super-admins, harden on cloud (#9739)
* 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>
2026-06-23 20:50:40 +00:00
Ruben Fiszel e90b2be8fa perf(monitor): skip protected prefix in retention delete via cross-batch watermark (WIN-2088) (#9744)
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>
2026-06-23 20:49:55 +00:00
Ruben Fiszel 984ea728d9 fix: pipeline annotation false-positives from body comments (#9736)
* 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>
2026-06-23 12:35:37 +00:00
hugocasa ba4b368706 fix: prevent variable push from corrupting is_secret variables (#9705)
* 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>
2026-06-23 12:45:36 +02:00
Ruben Fiszel 723a65920f chore(main): release 1.737.0 (#9728)
* chore(main): release 1.737.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-06-23 12:10:15 +02:00
Ruben Fiszel c644311eca fix(ext-jwt): reject external JWT auth for non-existent workspaces (#9723)
* 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>
2026-06-23 11:25:23 +02:00
Ruben Fiszel 31d9215e5a fix: bound orphan-cleanup drain rate with capped multi-batch loop (#9730)
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>
2026-06-23 11:24:36 +02:00
Ruben Fiszel 75bafabeee perf(monitor): hash active-root exclusion in retention delete (WIN-2088) (#9732)
* 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>
2026-06-23 11:23:50 +02:00
Ruben Fiszel fa3596885b fix: allow SQL args in managed // materialize scripts (#9733)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:13:04 +02:00
Ruben Fiszel 6d94865109 fix: optimize cleanup_job_perms_orphaned and job_result_stream cleanup queries (#9727)
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>
2026-06-23 10:14:40 +02:00
Ruben Fiszel 8dea38383f fix: prevent silent audit-partition outage via monitor watchdog + alert (#9729)
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>
2026-06-23 10:13:29 +02:00
hugocasa 2879cbb65a feat(apps): opt-in sandbox isolation for published & raw apps (alpha) (#9420)
* 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>
2026-06-23 10:05:00 +02:00
Ruben Fiszel e82a6a6830 chore(main): release 1.736.0 (#9720)
* chore(main): release 1.736.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-06-23 09:44:35 +02:00
Ruben Fiszel e16061df06 fix(health): detect read-only replica via pg_is_in_recovery() (#9722)
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>
2026-06-23 09:37:29 +02:00
centdix 6f4017d694 feat(ai-chat): workspace AI chat skills (SKILL.md upload + read_skill tool) (#9648)
* 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>
2026-06-23 00:16:13 +02:00
hugocasa e19594df2a fix: re-enforce scoped API token boundaries across handlers (#9712)
* 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>
2026-06-22 23:55:19 +02:00
Ruben Fiszel 83ec0dd07a chore(main): release 1.735.0 (#9700)
* chore(main): release 1.735.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-06-22 14:07:47 +02:00
hugocasa e403f92d7e fix: enforce job_dir containment when writing module files (#9703)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 13:14:14 +02:00
Ruben Fiszel 8a0b0abead fix: ignore NotFound errors when deleting log files from object store (#9707)
* 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>
2026-06-22 13:12:29 +02:00
hugocasa ef4962e52a fix(oauth): restore bring-your-own CC token URL override (#9711)
* 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>
2026-06-22 13:05:15 +02:00