The app provenance gate short-circuits on a valid presigned signature, but only the raw download_s3_file route parsed it. The parquet/csv/table-count/file-preview/metadata routes discarded sig/exp and always fell through to the provenance gate, so a presigned S3 object rendered as a table showed "File restricted" for any viewer who did not produce it. Thread sig/exp through every apps_u S3 display route and forward the presigned bearer from ParqetCsvTableRenderer/DisplayResult.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): keep session-exit URL clean by syncing new_draft strip with the router
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(frontend): correct replaceState comment and test-mock wording per review
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(frontend): correct replaceState comment and drop drafting-history phrasing
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): prevent runs timeframe calendar popover overflow on small screens
The Runs page timeframe picker rendered its popover as a wide 3-column row
(preset list + two side-by-side calendars). With the right-aligned trigger and
a center-anchored `bottom` placement, the popup ran off the right edge on
narrow viewports.
Anchor the popover to the right edge (`placement="bottom-end"`) and make its
content reflow to a vertical stack below the `sm` breakpoint, capped at
`max-w-[calc(100vw-2rem)] max-h-[80vh] overflow-auto` so it can never exceed the
viewport. The desktop side-by-side layout is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): compact runs timeframe picker with a Start/End toggle on small screens
The two-calendar desktop popover needs ~780px (two min-w-9 grids + presets +
popover padding); below that it overflows. Under 800px, show a single calendar
with a Start/End toggle picking which bound it edits, using set-start/set-end so
each bound keeps its date and HH:MM time inputs — the same precision the desktop
start/end pair offers.
On short/landscape viewports the compact panel is scroll-contained within the
popover's fitViewport height (contentClasses overflow-y-auto, scoped to the
small layout) so its lower controls stay reachable. The desktop two-calendar
layout is unchanged.
Presets are shared between both layouts via a snippet, and the active range is
preserved across the breakpoint since both branches drive the same value.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): let InlineCalendarInput month/year selects portal, use in compact timeframe picker
Add an opt-in `portalSelects` prop to InlineCalendarInput that portals the
month/year dropdowns to the body (default keeps them in-flow, so existing
consumers are unchanged). The compact runs timeframe picker enables it so the
dropdowns escape its scroll-contained (overflow-y-auto) popover instead of
being clipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai): stop sending the AI agent system prompt twice for OpenAI
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai): document collect_system_prompt precedence and trim duplicate comments
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai): hoist only the leading system prompt for OpenAI
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
nsjail caps a jailed job's virtual address space at rlimit_as (4096 MiB for
python3 and ansible). JIT runtimes (Bun/JavaScriptCore, the JVM) reserve large
virtual ranges up front, so a subprocess spawned from a jailed Python/Ansible
job can crash against this cap even when its physical memory use is modest
(e.g. the Bun-compiled claude CLI hitting JSC/pthread allocation failures).
Most other language protos already run with disable_rl: true (unlimited);
python3 and ansible are the outliers with an explicit rlimit_as. This exposes
that cap via a per-language env var (NSJAIL_PY_RLIMIT_AS_MB,
NSJAIL_ANSIBLE_RLIMIT_AS_MB) so operators can raise or lift it on a dedicated
worker pool without a source patch/rebuild and without weakening the
mount/PID/user-namespace isolation that provides the real security boundary.
Only the address-space limit changes; cpu/fsize/nofile rlimits are untouched.
Value is in MiB, or unlimited/none/inf/0 to uncap (rlimit_as_type: INF). Unset
keeps the historical 4096 default.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): invalidate cached app policy on change or deletion (GHSA-r5v4-cxh9-7qhq)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(agents): keep PR tests and comments minimal and non-ephemeral
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The published ghcr.io/windmill-labs/windmill-cli image shipped two fixable
CRITICAL findings:
- openssl (libssl3t64, openssl-provider-legacy): stale in the oven/bun:slim
base image (CVE-2026-34182). Fixed by running apt-get upgrade so the image
picks up the patched Debian packages.
- vitest 2.1.9 (CVE-2026-47429 / GHSA-5xrq-8626-4rwp): a dev-only
devDependency reference in esrap's cached package.json living in bun's
package download cache. The cache is unused at runtime, so it is removed
after install.
Validated by building the image and scanning with Trivy: openssl now reports
3.5.6-1~deb13u2 (fixed) and vitest is entirely absent. wmill still runs. The
only remaining CRITICALs are perl-base CVEs with no upstream fix available.
Fixes GIT-922
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A token scoped only to resources:write:<path> could delete linked secret
variables it had no variables:write scope for, by embedding $var:<victim>
in an attacker-controlled resource value and triggering the resource-delete
cascade. #9712 re-enforced scoped-token boundaries broadly but missed this path.
Add check_linked_var_delete_scopes, called before the cascade in both
delete_resource and delete_resources_bulk: require variables:write for every
linked variable, failing (and rolling back) the delete otherwise. No-op for
unscoped tokens, so full-token cascade cleanup is unchanged.
No co-located-path exemption: a resource and a variable may share a path, and a
resource-write token can create a resource over an existing standalone variable
and self-reference it, so "same path as the deleted resource" is attacker-
forgeable and cannot stand in for variable scope.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai): disable redirects on worker AI provider client (GHSA-5q4v)
The worker AI request path issued provider requests with the shared
HTTP_CLIENT, which follows up to 10 redirects without revalidating each
hop. SSRF validation on the provider base_url is single-shot, so a public
base_url could 3xx the worker into a private/internal host (e.g. cloud
metadata), bypassing the private-endpoint protection. The API proxy was
already hardened in #9370; the worker path was missed.
Add a dedicated AI_HTTP_CLIENT with redirects disabled and use it for the
user-controlled provider endpoint, mirroring the API proxy client.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai): add ALLOW_AI_BASE_URL_REDIRECTS debug escape hatch
Off by default (redirects stay disabled). When set, restores redirect
following on the AI HTTP client for debugging non-standard/self-hosted
gateways, with a startup warning that it weakens SSRF protection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(ai): correct redirect comment for the escape hatch override
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(ai): condense redirect invariant comments per review
Anchor the SSRF rationale to ALLOW_AI_BASE_URL_REDIRECTS (the knob that
would break it) and shorten the AI_HTTP_CLIENT and call-site comments to
avoid restating it at multiple sites (AGENTS.md).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dev-workspace): reflect existing protection rules in lock toggles
When creating or attaching a dev workspace, the "block direct edits" and
"prevent forking" toggles now check the root workspace's current protection
rules. If a restriction is already enforced by an existing rule, its toggle is
shown on but locked, with a note, instead of offering a fresh default that could
misrepresent the effect. The value sent to the backend is derived so it stays
consistent with what the locked toggle shows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: clarify fail-open comment on dev-workspace lock toggles
Reword the protection-rule fetch comment so the fallback path isn't misread as
dropping protection: a failed fetch falls back to the editable default-on
toggle, and any real rule still enforces server-side.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(dev-workspace): lock protection toggles until rules load
The lock toggles derived alreadyBlocks* from an async fetch, so during the load
window (and the first frame before loading flips) they were editable and the
effective value could be false. A user could turn a lock off and submit before
an existing rule was detected, omitting the reserved rule and silently leaving
prod unprotected once that existing rule was later removed.
Treat "rules not yet known" (loading || current === undefined) the same as
"already enforced": lock the toggle on and keep the effective value true during
that window, so the request can never submit false before the fetch resolves.
Submission stays available (a hung fetch degrades to over-protection, not a
blocked form). Also fixes the stale-value flash when switching base workspace.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(dev-workspace): honor rule bypasses and guard stale protection fetches
Two issues in the protection-rule awareness for the dev-workspace lock toggles:
- Bypassable rules became unconditional locks. alreadyBlocks* used
isRuleActiveInRulesets, which ignores bypass_users/bypass_groups, and forced
the request flag to true. The reserved dev_workspace_lock rule is created with
empty bypass lists, so layering it over an existing rule that let specific
users through revoked their deploy/forking access. Switch to
isRuleUnconditionallyActiveInRulesets so a toggle is only shown as already
enforced (locked) when an existing rule has no bypasses; a bypassable rule
stays editable, making the lock the user's explicit choice.
- A stale protection fetch could apply another base's rules. The generated
client can't take an abort signal, so a delayed response for a previous base
could overwrite the newly selected one. Tag each result with its workspace and
only trust a result matching the current base; also throw AbortError from a
superseded fetch so it can't overwrite current.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: condense protection helper comment to four lines
Trim the isRuleUnconditionallyActiveInRulesets doc comment to satisfy the
AGENTS.md ≤4-line comment rule.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(dev-workspace): align already-enforced note under the toggle label
The note used ml-8, landing under the toggle switch rather than aligned with
the switch edge or the label, so it read as floating. Bump to ml-11 so it lines
up under the label as helper text for that toggle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
go_on_release.yml referenced this third-party action by the mutable @devel
branch in the step that holds secrets.DENO_PAT (a write-scoped PAT used to push
the generated go-client to another repo). Pinning to a full commit SHA (v1.7.3)
removes the mutable-ref supply-chain exposure, consistent with the other
SHA-pinned actions in the repo.
* feat(nativets): expose standard web-platform globals for bun parity
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(nativets): wire bun-present Event subclasses and add construction smoke test
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(nativets): seed performance.timeOrigin per isolate, drop broken reportError
Addresses CI Codex review on #10112:
- performance.timeOrigin was undefined (setTimeOrigin never called); seed it
per isolate via __wmInitPerIsolate executed from create_nativets_runtime.
- reportError needs a global EventTarget this runtime never installs; drop it.
- reword the namespace-import comment to not describe drafting history.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(nativets): wire DOMException global + broad edge-case smoke sweep
DOMException is present in bun and, more importantly, deno_web references it as
a global: AbortController.abort() with no reason constructs a
DOMException("...", "AbortError"), so the already-wired AbortController/
AbortSignal threw "DOMException is not defined" on abort. Surfaced by a new
functional edge-case sweep (smoke_web_globals_edge_cases) that exercises every
wired global for real (not just presence) — DOMException/abort, AbortSignal.timeout,
EventTarget dispatch, stream tee/reader/writer, all 3 compression formats,
structuredClone Map/Set/Date/circular/reject-function, performance mark/measure,
MessagePort delivery — plus a check that the merged Web Crypto globals still work.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(nativets): restore arg-default smoke tests dropped in merge, drop history comments
Addresses CI Codex/Pi review on the merge commit:
- Merge conflict resolution (checkout --ours) dropped smoke_missing_optional_arg_uses_default
and smoke_explicit_null_arg_is_preserved (added on main by #10111); restore them.
- Reword edge-case-sweep comments to state the constraint, not how the gaps were found.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(nativets): give reportException a global dispatch target; wire stream reader/controller globals
Addresses CI Codex review on #10112:
- P1: a throwing EventTarget listener (and reportError) is routed through
deno_web's reportException, which dispatches on a saved global reference.
With none set, dispatchEvent threw a masking error that hid the original.
Wire a dedicated EventTarget as that target so the ORIGINAL error is reported
(async unhandled, matching bun). Does NOT make globalThis an EventTarget (bun's
isn't either). Re-adds reportError, now functional. Regression test asserts the
original error is surfaced, not a masking one.
- P2: wire the stream reader/controller globals bun also exposes
(ReadableStreamDefaultReader/BYOBReader, ReadableStreamDefault/ByteStreamController,
ReadableStreamBYOBRequest, WritableStreamDefaultWriter/Controller,
TransformStreamDefaultController) for instanceof parity; sweep verifies via real
reader/writer/controller instances.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(nativets): make globalThis an EventTarget so globalThis.reportError() works
Addresses follow-up CI Codex review on #10112:
- P1: the prior fix saved a *separate* EventTarget as the global reference, so
globalThis.reportError() still failed its receiver check (this === globalThis_)
with 'Illegal invocation'. Make globalThis itself the saved reference by turning
it into a functional EventTarget (setPrototypeOf to DedicatedWorkerGlobalScope +
setEventTargetData + webidl brand + saveGlobalThisReference), per isolate in
__wmInitPerIsolate. Both reportError(e) and globalThis.reportError(e) now surface
the original error (async, matching bun) instead of throwing. New test
smoke_report_error_both_call_forms covers both call forms.
- P2: reword the regression-test comment to state the invariant, not the patch history.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(nativets): wire performance constructor globals for bun parity
Addresses the P2 nit in the CI Codex review: bun exposes Performance,
PerformanceEntry, PerformanceMark, and PerformanceMeasure as globals (deno_web
exports all four), so wire them alongside the performance singleton. The
edge-case sweep verifies instanceof against real mark/measure entries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(nativets): state global-wiring comment as a constraint, not patch history
Addresses the P2 in the CI Codex review: reword the block comment to describe
the current bun-parity constraint and the deliberate EventSource/ImageData
exclusions, without narrating what was or wasn't wired before (per AGENTS.md).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(jseval): raise QuickJS eval memory cap to 128MB with clear OOM error
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(jseval): note bare null/undefined throws are absorbed into OOM bucket
Addresses CI review P2 nit on map_quickjs_error.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(jseval): keep batch-rerun eval on a conservative 32MB cap; tighten OOM match
Addresses CI review: eval_simple_js runs in the API process with unbounded request concurrency, so it must not inherit the raised flow-transform cap. Tighten the Exception OOM match to exact string. Reword drafting-history comments per AGENTS.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(jseval): gate OOM on InternalError kind; path-specific remediation hint
Require the OOM InternalError name (not just the message) so a user throw new Error('out of memory') is not misclassified, and only suggest QUICKJS_MEMORY_LIMIT_MB on the env-tunable flow path (not the fixed-cap eval_simple_js path).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Flow input schemas omit `required` (they carry an `order` key instead),
which made `serde_json::from_str::<SchemaType>` fail in
`convert_schema_to_schema_type`. The error was swallowed and callers fell
back to an empty `SchemaType::default()`, so MCP flow tools advertised no
inputs. Add `#[serde(default)]` to `type`, `properties`, and `required` on
`SchemaType` so these schemas deserialize correctly. Scripts always include
`required` and were unaffected.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(rls): wrap session GUC reads in RLS policies for per-statement InitPlan
RLS policies read current_setting('session.user' / 'session.groups' /
'session.pgroups' / 'session.folders_read' / 'session.folders_write')
directly inside their USING / WITH CHECK predicates. Postgres treats those
unwrapped calls as potentially row-varying and re-evaluates them once per
scanned row, on the read path of every workspace-scoped table.
The GUCs are set with SET LOCAL (set_config(..., true)) in
set_session_context(), so they are constant for the duration of a statement.
Wrapping each session-derived subexpression in a scalar sub-select lets the
planner hoist it to a one-time InitPlan (evaluated once per statement, reused
for every row) — same rows in, same rows out, N per-row GUC lookups collapse
to 1. Array-producing subexpressions keep an explicit ::text[] cast on the
sub-select so `= ANY (...)` / `?|` stay in their array-operand form rather
than being reparsed as a row-returning subquery.
The consolidating migration recreates every existing policy (across ~30 prior
migrations) whose predicate reads a session GUC, by deparsing the current
predicate and substituting the wrapped forms; the down migration is the exact
inverse (byte-identical round-trip). The adding-a-trigger skill documents the
wrapped form so new trigger tables inherit it.
Surfaced by pgrls (PERF001).
Fixes GIT-919
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(adding-a-trigger): fix RLS example cast placement for = any context
The `= any(...)` example put the ::text[] cast inside the sub-select, which
Postgres parses as a row-returning subquery and rejects at CREATE POLICY with
`operator does not exist: text = text[]`. Move the cast outside the sub-select
(matching the migration's canonical form) so the operand stays in array form,
and note why.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The nativets in-process runtime (deno_core) exposed no Web Crypto API:
`crypto` was undefined, so scripts could not use `crypto.getRandomValues`,
`crypto.randomUUID`, or `crypto.subtle`, even though the bun runner provides
them. This closes that parity gap by registering the `deno_crypto` extension
and wiring the crypto globals onto `globalThis`.
- Pin `deno_crypto = "0.223.0"`, the sibling release of the already-pinned
deno_core 0.352 / deno_web 0.240 stack (deps: deno_core ^0.352,
deno_web ^0.240, deno_error =0.6.1), so the rest of the deno stack is
untouched.
- Register `deno_crypto::init(None)` after `deno_web` in both the snapshot
(build.rs) and the runtime (lib.rs) extension lists, keeping the snapshot a
prefix of the runtime list. deno_crypto declares deps = [deno_webidl,
deno_web], which the position satisfies.
- Import `ext:deno_crypto/00_crypto.js` in runtime.js and assign
`crypto` / `Crypto` / `CryptoKey` / `SubtleCrypto` to `globalThis`.
- Add the `smoke_web_crypto` opt-in smoke test asserting the UUIDv4 shape of
`randomUUID`, a non-zero `getRandomValues` fill, and the known
SHA-256("abc") vector via `subtle.digest`.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(mcp): let MCP tokens call preview run tools (jobs:run scope)
The MCP proxy mints an internal JWT scoped to exactly `scope_for_route`
for the endpoint it forwards to. For preview run routes
(`run/preview`, `run/preview_bundle`, `run/preview_flow`,
`run_wait_result/preview`, `run_wait_result/preview_flow`),
`determine_kind_from_route` matched the `SCRIPT_JOBS` prefix
`jobs/run_wait_result/p` (because "preview" starts with "p") and derived
`jobs:run:scripts`. But the preview handlers run arbitrary request-supplied
code with no deployed path and require the broad `jobs:run` scope, so
`jobs:run:scripts` was rejected with 403 "Required scope: jobs:run".
Preview/bundle routes now carry no runnable kind, so the derived scope is
the broad `jobs:run` the handlers expect. This also aligns the route-level
access check with the handler check for these routes.
Fixes GIT-920
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(mcp): anchor preview-route match to endpoint segment
Address CI review: `route_path.contains("preview")` also matched by-path
runs of a deployed runnable whose path contains "preview" (e.g.
`jobs/run_wait_result/p/f/team/preview_report`). Since determine_kind_from_route
also feeds check_route_access, such a route would derive the broad `jobs:run`
and reject a legitimately kind-scoped `jobs:run:scripts:*`/`jobs:run:flows:*`
token with 403.
Anchor the exception to the actual preview endpoints
(`jobs/run/preview*`, `jobs/run_wait_result/preview*`) so by-path runs keep
their kind. Add regression tests for preview-named by-path paths, and trim
the comments per AGENTS.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to GIT-903 / PR #10106. That PR migrated the caddy-l4 image to
mholt's native `layer4` Caddyfile support, which required bumping the
Caddy base image to 2.11.4.
End-to-end testing (running the image and proxying real traffic, not
just `caddy adapt`) revealed that caddy >= 2.9 changed how `bind` treats
an empty argument. The shipped Caddyfile has `bind {$ADDRESS}` inside the
`{$BASE_URL}` site, and docker-compose leaves ADDRESS unset -- the
default self-host case. On 2.11.4 the empty `bind` makes Caddy drop the
entire `{$BASE_URL}` site, so the container listens only on :25 (layer4)
and the :80 HTTP reverse proxy to windmill_server silently disappears.
config-only checks (adapt/validate/boot) pass, so only real traffic
surfaces it.
Fix in the Caddyfile rather than downgrading Caddy (which would
reintroduce known CVEs on an internet-facing proxy): default the bind to
all interfaces when ADDRESS is unset via `bind {$ADDRESS:0.0.0.0 ::}`.
When ADDRESS is set it is honored unchanged; when unset the site binds
IPv4 + IPv6, matching the pre-2.9 behavior.
Verified on the caddy:2.11.4 image with a mock windmill_server backend
(HTTP :8000 + layer4 echo :2525):
- ADDRESS unset -> :80 and :25 both bind; HTTP and layer4 both proxy
- ADDRESS=0.0.0.0 -> same
- ADDRESS=127.0.0.1 -> HTTP site binds 127.0.0.1:80 (knob preserved)
Fixes GIT-903
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The self-hosted Caddy image relied on the abandoned
RussellLuo/caddy-ext/layer4 shim to provide the `layer4` Caddyfile
global option, alongside an old (May 2024) pin of mholt/caddy-l4 that
predated native Caddyfile support. This combination is fragile:
- If the image is ever built without the RussellLuo shim, the `layer4`
global option disappears and Caddy fails with
"unrecognized global option: layer4" — the reported bug.
- Bumping mholt/caddy-l4 to any version with native Caddyfile support
makes both modules register `layer4`, panicking at startup with
"global option 'layer4' already registered".
mholt/caddy-l4 now natively registers the `layer4` global option, so
drop the RussellLuo dependency entirely and switch the Caddyfile to the
native `route { proxy { upstream ... } }` syntax. The adapted layer4
JSON is byte-identical to the previous output, so runtime behavior is
unchanged.
Also bump the Caddy base image to 2.11.4 (required by current
caddy-l4) and add a path-filtered push trigger so the published
`:latest` image is rebuilt whenever the Caddy Dockerfile changes,
instead of only on manual dispatch (which is how `:latest` drifted out
of sync with the checked-in Caddyfile in the first place).
Fixes GIT-903
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): size AI-created flow notes to fit their text
Free notes created via the flow AI chat omit `size` (the tool prompt tells
the model to let the editor size them). validateFlowNotes seeded a fixed
275x60 box, but free notes never grow to fit content, so multi-line markdown
overflowed the box. Estimate height from the text instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): stack auto-placed flow notes by height to avoid overlap
Auto-placed free notes were staggered by a fixed index*84px step, but notes
can now be up to 600px tall, so consecutive generated notes overlapped. Track
a running y-cursor and advance it by each note's real height.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): advance note stack cursor past preserved column notes
A round-tripped note keeps its existing auto-column geometry ({-375, y});
the stack cursor ignored it, so a newly added geometry-less note landed on
top. Preserved notes overlapping the auto-stack column now advance the cursor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(ai-chat): trim estimateFreeNoteSize comment per AGENTS.md
Keep only the non-obvious fixed-height renderer constraint; drop the
implementation narration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-agent): use a real tool description instead of the tool name
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-agent): render tool-name error full width and hoist it above the description
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-agent): make tool description field hug its content so a single line is vertically centered
Add an optional minHeight param to the autosize action (default unchanged at 30px) and pass minHeight 0 for the tool description so an empty/one-line field no longer reserves the 30px floor and leaves dead space below the text.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(ai-agent): regenerate OpenFlow-derived prompts, CLI guidance, and copilot zod schema for tool description
Fixes the check-freshness CI failure (system_prompts + skills.gen.ts) and makes the flow copilot's openFlow.json / openFlowZod.gen.ts aware of the new AgentTool.description field so AI-authored tools can set it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): cancel pending draft-prompt flush on delete
deleteSession removed the record from memory and IndexedDB but left the
debounced draft-prompt flush timer running; it would fire afterward and
persistTouched the deleted session back into IndexedDB, resurrecting a
draft deleted inside the 400ms window on the next reload. Clear the
per-session timer in deleteSession.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): de-transient drafts on keystroke; count pending in workspace teardown
Two follow-ups to the pending-sessions feature (#10076), surfaced by codex review:
- setSessionDraftPrompt clears `transient` synchronously so a draft typed into
is no longer treated as a reusable blank by createSession. Previously the flag
only cleared 400ms later via the debounced flush, so pressing `+` right after
typing reopened the same draft instead of spawning a second pending session.
Only the IndexedDB write stays debounced.
- countSessionsForWorkspace counts on `workspace_id ?? pending_workspace_id`, so
the archive/delete confirmation includes persisted unsent drafts, matching
reconcileSessionsLifecycle which tears them down alongside committed sessions.
Adds regression tests that drive the real keystroke transition and the pending
draft count.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): reuse-guard on draftPrompt, not a synchronous transient clear
Addresses codex-review P1 on #10087: the previous approach cleared `transient`
synchronously on a keystroke to stop createSession reusing a just-typed draft.
But `transient` also means "in-memory only, not yet in IndexedDB" — clearing it
before the 400ms flush left the draft in neither bucket, so a reconcile landing
inside the window (hydrateSessions rebuilds the list as in-memory-transients +
DB rows) dropped the unsaved draft and dangled currentSessionId.
Separate the two concepts instead: keep `transient` as pure persistence state
(the draft survives hydration), and define a reusable blank as
`transient && !draftPrompt`. createSession's reuse probe and its non-reuse drop
both key on isReusableBlank, so a typed-but-unflushed draft is neither reused nor
discarded, and setSessionDraftPrompt no longer touches `transient`.
Adds a regression test that interleaves a first-touch debounce with reconcile
and asserts the draft stays in memory (and currentSessionId intact); updates the
keystroke test to the real transient-preserving transition.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): treat a typed-then-erased draft as touched, not a reusable blank
Addresses codex-review P2 on #10087. isReusableBlank used `!s.draftPrompt`, so a
draft typed into then erased back to '' (draftPrompt === '', flush still pending)
was classed as a reusable blank: pressing `+` within 400ms reused it, but after
the flush cleared `transient` the same `+` created a new session — inconsistent
across the debounce boundary, and in another family the non-reuse drop removed
the draft while its pending timer later persisted it back.
setSessionDraftPrompt only sets draftPrompt on a genuine edit (mount-time '' is a
no-op via the equality guard), so `draftPrompt === undefined` cleanly means
"never edited". Key isReusableBlank on that instead of falsiness.
Adds a type-then-erase-before-`+` regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(sessions): trim new comment blocks to AGENTS.md 4-line limit
Addresses codex-review P2 on #10087: condense the setSessionDraftPrompt,
countSessionsForWorkspace, and isReusableBlank comment blocks to <=4 lines per the
AGENTS.md rule. Comment-only, no logic change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(sessions): trim last two comment blocks to AGENTS.md 4-line limit
Follow-up to codex/pi P2 nits on #10087: condense the keystroke-transition test
comment (5→4 lines) and the countSessionsForWorkspace comment (→3 lines). All new
comment blocks in the PR are now ≤4 lines. Comment-only, no logic change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The MCP `get_items` script filter used `auto_kind IS NULL`, which excluded
every script with a non-null `auto_kind` (pipeline, test, WAC, ...). These
are valid runnable scripts and should surface as MCP tools.
Switch to the deny-list `(auto_kind IS NULL OR auto_kind <> 'lib')`, matching
the scripts list API (windmill-api-scripts). Only library scripts (no main
function) are excluded; pipeline/test/WAC and any future auto_kind values are
included.
Fixes WIN-2190
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): stop spurious raw-app reload that 404s on "Start without AI"
Creating a new raw app and clicking "Start without AI" surfaced an "App not
found" toast. The page's load effect re-ran loadApp() mid-bootstrap and fetched
the draft via getAppByPath before the first autosave POST had landed → 404.
Root cause: the effect used the legacy run() from svelte/legacy without
untrack, so loadApp()'s synchronous reactive read of the draft-hint SvelteMap
(getLocalDraftHint via shouldSeedNewDraft, added in #10044) subscribed the
effect. The first autosave optimistically flips that hint (#9351) before its
debounced POST, re-firing the effect → spurious loadApp() → getAppByPath on a
not-yet-persisted draft.
Convert the block to $effect + untrack so it depends only on page.params.path /
$workspaceStore, matching the sibling apps/edit and flows/edit routes. Autosave
and draft persistence are unchanged; only the phantom reload is removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(frontend): tighten untrack invariant comment to ≤4 lines
Per AGENTS.md comment policy (Codex review nit).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): allow enabling sandbox isolation before first deploy
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): allow setting public access mode before first deploy
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: custom tags on CLI runs, show previews in default runs view
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* revert: don't include previews in default runs view
Deferring the runs-view UX change; keeping only the CLI --tag work.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): forward --tag for codebase/bundle script previews
The bundled-preview branch posts a multipart payload to
/jobs/run/preview_bundle; --tag was only wired into the non-bundled
runScriptPreview call, so codebase previews silently used the default
tag. Include tag in the preview payload (backend reads preview.tag).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): port flow-group and sticky-note instructions to global mode
Global-mode AI chat inherited only the bare FlowGroup schema and had no
sticky-note support, so it never proactively segmented flows into groups
and could not author flow-wide notes. Flow mode carried this guidance
inline in its own prompt and set_flow_json tool.
Bring global mode to parity:
- Enrich write_flow's `groups` description (color palette + fields) and add
a `notes` field mirroring flow mode's set_flow_json.
- Thread `notes` through editableFlowToDraftValue and the write_flow handler
so notes reach FlowValue.value and survive the deploy round-trip. Reads and
patch_flow_json already carried notes via the shared editableFlowJson helpers.
- Expand getFlowInstructions with the groups/notes organizing guidance
(strongly-recommended proactive grouping, color palette, when-to-use-which)
and mention notes in the write/read/compact-view/structural-edit bullets.
Add a write_flow -> read_workspace_item notes round-trip test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(ai-chat): trim write_flow groups/notes schema descriptions
The write_flow tool schema is re-sent every chat loop iteration, so the
verbose groups/notes descriptions were a per-iteration token tax that
duplicated the on-demand getFlowInstructions() prose. Trim the .describe()
calls to the correctness-critical bits (color palette, type "free", null
semantics) and point to get_instructions for the full field reference,
which getFlowInstructions() already carries.
Addresses CI review feedback (Claude + Pi).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Resources page dispatches per-tab data loads from the Tabs
`on:selected` handler and `reload()`, but both only handled `cache`
and `states` — selecting the Theme tab never called `loadTheme()`, so
`themeResources` stayed undefined and the tab rendered empty even
though app themes existed. The reload `$effect` reads `tab` inside
`untrack`, so it didn't re-fire on tab change either (only a filter or
workspace change did, which is why typing in the filter "fixed" it).
Add the missing `theme` branch in both places.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"Re-run all jobs matching filters" selects completed jobs via list_filtered_uuids
windowed by started_before/started_after (the timeframe). v2_job_completed has no
index on started_at (only completed_at), so that filter alone forces a
workspace-wide seq scan — a query observed at ~48s on a large instance.
started_at >= minTs implies completed_at >= minTs (a job completes at/after it
starts), so adding completedAfter = minTs is a lossless bound: it drops no row the
started_at window keeps, but lets the (workspace_id, completed_at DESC) index start
the scan at the window's lower edge instead of scanning the whole table. The
selected cohort is unchanged (started_at stays the exact filter); this is purely a
plan improvement. EXPLAIN: seq scan -> completed_at index scan.
Not completedBefore: a job can start in-window but finish after maxTs, and bounding
completed_at above would drop it. Scoped to re-run; batch cancel (v2_job_queue,
small) is untouched.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sessions): support many pending sessions persisted in IndexedDB
Allow several unsent AI sessions to be set up in parallel. Split the
transient flag into "in-memory, not yet persisted" (unsent is derived
from workspace_id), persist a pending session to IndexedDB on first
touch with its own draftPrompt, show pending sessions in the sidebar
under the family filter, and reconcile them by pending_workspace_id.
The + button reuses the untouched draft in the active family so idle
clicks don't pile blank entries; touching one spawns a fresh blank.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sessions): focus composer when + reuses the untouched draft
When there are no pending changes, `+` reuses the active family's untouched
draft instead of creating a new session (unchanged). But when the reused draft
is the one already on screen, currentSessionId doesn't change, so nothing
navigated and the click gave no feedback. Bump a composerFocusRequest nonce in
the reuse branch and have SessionWrapper's focus effect depend on it, so the
composer re-focuses and the user can type right away.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): per-session debounce for draft prompt flush
A single module-level flush timer let a keystroke in one pending draft
cancel a sibling draft's pending first-touch flush, so the earlier draft
was never written and its typed prompt vanished on reload. Key the
debounce per session so parallel drafts persist independently. Also
collapse the touch rationale repeated across the preview-tab/collapse/size
setters onto persistTouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(saml): add ALLOW_PRIVATE_SAML_METADATA_URLS SSRF bypass
Introduce the ALLOW_PRIVATE_SAML_METADATA_URLS env var and its
allow_private_saml_metadata_urls() helper, mirroring the existing
ALLOW_PRIVATE_MCP_SERVER_URLS opt-out. This lets self-hosted deployments
with internal SAML IdPs (private IPs, no public DNS) skip the metadata-URL
SSRF check that otherwise blocks server startup.
The companion EE change (saml_ee.rs) consumes the helper to gate the
validate_url_for_ssrf() call and additionally treats a cleared
(empty/whitespace-only) SAML_METADATA setting as no SAML configured.
Fixes WIN-2169
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(saml): surface opt-in hint and record SSRF control in threat model
Add saml_ssrf_error_message() so private-IdP metadata URL rejections point
to ALLOW_PRIVATE_SAML_METADATA_URLS (mirroring the MCP helper), with a unit
test. Record the new SSRF opt-in under T2 in THREAT_MODEL.md, and bump the
EE ref for the companion saml_ee.rs change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(saml): add validate_saml_metadata_url with opt-in unit tests
Factor the SAML metadata SSRF gating into validate_saml_metadata_url()
(mirroring validate_mcp_server_url) so the private-URL opt-in branch is
unit-tested at the ssrf layer: blocks private by default, allows on
true/1, and keeps scheme/host syntax guards when the opt-in is on. Bump
the EE ref for the companion saml_ee.rs change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 394ad23242de429aef4074cc1dc28867dac95870
This commit updates the EE repository reference after PR #659 was merged in windmill-ee-private.
Previous ee-repo-ref: 86da208c5aef2570568e18c7ab98f4d58adeec18
New ee-repo-ref: 394ad23242de429aef4074cc1dc28867dac95870
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* ci: run Codex review on fork PRs when a maintainer triggers it
The fork skip in codex-pr-review.yml unconditionally bailed on
cross-repository PRs, so even a maintainer's /codex or /review comment
(routed through pr-review-commands.yml via workflow_call, gated by
check-write-access) skipped external PRs.
Gate the skip on the automatic pull_request trigger only, detected via
an empty INPUT_PR_NUMBER (the metadata step already branches on this at
the same step). The workflow_call path now reviews fork PRs; the auto
pull_request trigger still skips them.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: run Pi review on fork PRs when a maintainer triggers it
Apply the same fork-skip gating as the Codex review: skip fork PRs only
on the automatic pull_request trigger (empty INPUT_PR_NUMBER), so a
maintainer's /pi or /review comment (workflow_call, gated by
check-write-access) reviews external PRs.
Claude's pr-ready-review.yml needs no change: it has no fork skip, checks
out main (not the fork ref), and reviews via gh pr diff/view with a
restricted tool allowlist, so it already handles fork PRs on the command
path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: harden fork-review path against secret exfiltration
Addresses the CI review of the fork-review enablement. On the fork path
(maintainer-triggered workflow_call for a cross-repository PR), the
reviewer ran an autonomous agent over the attacker-controlled merge
checkout with the EE token present, full-access sandbox, and the review
prompt itself read from that untrusted checkout — so a malicious fork
could rewrite the reviewer's own instructions to exfiltrate secrets.
For fork PRs only (detected via the is_fork step output):
- withhold WINDMILL_EE_PRIVATE_ACCESS: skip the EE access/checkout/
substitution steps, so the private-repo token is never in the env.
- read REVIEW.md and the prompt file from the trusted base ref
(git show origin/<base>:...) instead of the merge checkout.
- restrict the agent: Codex runs with -s workspace-write (network off)
instead of danger-full-access; Pi drops the bash tool.
Non-fork PRs are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: redact provider credentials from fork review comments
The model call needs the provider credential in its environment/config, so a
network-disabled sandbox alone can't stop a prompt-injected fork review from
reading the key (Codex: $HOME/.codex/auth.json; Pi: /proc/self/environ) and
emitting it in the final message, which both workflows post verbatim. GitHub
Actions log masking does not cover comments posted via the API.
Strip the known credential values (OpenAI key + raw Codex auth JSON and its
nested tokens; DeepSeek key) from the review body before posting, closing the
comment as an exfiltration channel. Applied unconditionally since a credential
should never appear in a review comment regardless of trigger.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: don't persist github.token in fork review checkout
actions/checkout writes github.token into .git/config (http.extraheader) by
default. The review agent can read the checked-out tree, so on the fork path a
prompt injection could exfiltrate that token (issue/PR write) via .git/config —
the provider-credential redaction added earlier didn't cover it.
Set persist-credentials: false on the merge-ref checkout so the token is never
written to disk. Safe on both paths: the only later git op is an unauthenticated
fetch from the public origin, EE checkout uses its own token, and gh uses
GH_TOKEN. Also redact github.token from the posted comment as defense-in-depth.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: disable Pi project-local discovery on fork reviews
Pi auto-discovers and executes project-local .pi extensions (.ts/.js) at
startup with DEEPSEEK_API_KEY in its environment — before the --tools allowlist
applies — so a fork could add an extension that exfiltrates the key over the
network, which output redaction can't catch.
On the fork path (cwd is the fork checkout), pass --no-extensions to disable
extension discovery, plus --no-skills/--no-prompt-templates/--no-themes/
--no-context-files so fork-controlled skills, templates, themes, and
AGENTS.md/CLAUDE.md aren't auto-loaded into the reviewer's prompt as an
injection vector. Non-fork behavior unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: use unguessable delimiter for untrusted PR metadata outputs
The PR title/body were written to $GITHUB_OUTPUT with a fixed heredoc
terminator (PR_BODY_EOF). A fork author could embed that terminator in their PR
body to close the heredoc early and append their own output lines — e.g.
is_fork=false, which (last-write-wins) overrides the real is_fork=true and puts
fork code back on the trusted path (EE checkout + substitute_ee_code.sh with the
private token, full-access agent).
Generate a per-run random delimiter (128 bits from /dev/urandom) for the title
and body heredocs so the terminator can't be predicted or embedded. Everything
else in the block is single-line and newline-free, so this closes the injection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: set PI_OFFLINE=1 on fork Pi reviews to block package resolution
--no-extensions only filters which resources are *loaded*; Pi still resolves
packages declared in a fork's .pi/settings.json first, running `npm install` /
the configured npmCommand and lifecycle scripts with DEEPSEEK_API_KEY in env and
network available — before the extension filter applies.
Set PI_OFFLINE=1 on the fork path so the resolver's installMissing() short-
circuits (returns false) for every missing package, skipping all install/clone/
lifecycle execution. It gates only startup network ops (installs, helper-binary
downloads), not the provider inference call, so the review still runs. Verified:
a fork .pi/settings.json with a malicious npmCommand does not execute under the
flag. Non-fork path unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: run fork Pi review from an isolated dir to cut off project config
Root cause of the recurring fork-review exposure: Pi resolves every project
config from <cwd>/.pi — settings/packages, extensions, skills, themes, prompts,
SYSTEM.md, APPEND_SYSTEM.md — so running inside the fork checkout let a fork
inject any of them to execute code or rewrite the reviewer's system prompt with
DEEPSEEK_API_KEY in env. Per-flag opt-outs (--no-extensions, PI_OFFLINE, ...)
only covered discovered vectors one at a time (SYSTEM.md wasn't covered).
Discovery is cwd-based (single level, no walk-up; global fallback is the trusted
runner home), so run Pi from a fresh mktemp dir where no fork .pi/* is on the
path. The fork agent has no shell, so pre-compute the diff (base...head SHAs are
trusted) into the context file it reads; it may still read fork files by
absolute path for extra context — reads are safe, only config discovery and code
execution were the risk. Outputs now use absolute workspace paths since cwd
moved. The --no-* flags and PI_OFFLINE stay as belt-and-suspenders. Non-fork
path unchanged. Verified: a fork .pi/SYSTEM.md sentinel is not discovered from
the isolated cwd.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: keep review artifacts outside the checkout to defeat symlink writes
Both workflows wrote generated files (final message, event stream, review
context, prior-comments) into $GITHUB_WORKSPACE. On the fork path the merge tree
is attacker-controlled, so a fork could commit any of those paths as a symlink
(e.g. codex-final-message.md -> ../../_actions/actions/github-script/v7/dist/
index.js). Our write would follow it and overwrite the next action's code, which
then executes with the provider credential and the write-capable GitHub token —
no prompt injection required.
Route every generated file through $RUNNER_TEMP, which is runner-created and
outside the checkout, so no fork-committed symlink is on the path:
- prior-comments.json and pr-review-context.md are written to RUNNER_TEMP; the
context step reads prior-comments from there.
- The agent is given the context file's absolute RUNNER_TEMP path (appended to
the prompt); prompt files updated to reference it instead of a checkout-
relative path. Pi (no shell on forks) gets the diff pre-computed into that
context file; the isolated-cwd hardening is retained.
- Codex writes -o to RUNNER_TEMP; Pi writes its events/final message there; both
post steps read from RUNNER_TEMP.
Non-fork behavior is functionally unchanged (trusted checkout; same review
inputs, now sourced from RUNNER_TEMP).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: condense fork-review comments to the 4-line limit
AGENTS.md requires each invariant stated in <=4 lines. Trim the security
comments added in this branch (fork-skip rationale, output delimiter, isolated
cwd, RUNNER_TEMP artifacts, credential redaction) to comply without dropping the
constraint each one records.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-agent): don't mark repeated tool calls as failed in flow graph
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ai-agent): cover reporter's mixed repeated-tool-call scenario
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): cover script/flow component outputs in deployed-app S3 provenance gate
Deployed apps read S3 files on-behalf of the app author for logged-in viewers
(#10048). A confused-deputy guard confines those reads to files the app
"produced", but the recent-production check only matched inline `appscript`/
`preview` jobs nested under the app path. Files produced by the deployed
script/flow components an app is wired to run (e.g. a SQL query persisted to S3)
were therefore denied "File restricted" for every viewer, admins included.
Expand the provenance check to also match completed `script`/`flow`/`flowscript`/
`flownode` jobs whose `runnable_path` is one of the app's declared triggerables,
and accept the author identity via `permissioned_as = on_behalf_of` (not only
`created_by = caller`) so files produced on-behalf of the author are covered.
Reads outside the app's declared triggerables stay denied.
Adds a regression test seeding a script-kind produced file that reproduces the
"File restricted" denial before the fix and passes after.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): key S3 provenance on on-behalf identity + cover flow steps (review)
Addresses the CI review on the S3 provenance gate:
- P1 (confused deputy): the recent-production check keyed on `created_by =
caller`, so a viewer who can run a declared script/flow directly (outside the
app, with un-pinned inputs) could craft a result naming an author-only key and
read it back through the app as the author. Key provenance instead on the
producing job's `permissioned_as` matching the on-behalf identity the download
reads as (the author in author-mode); a viewer's direct run has
`permissioned_as = viewer` and no longer clears the gate. Drops `created_by`
from both the appscript/preview and script/flow branches, closing the same
latent hole in the pre-existing inline-script branch.
- P2 (dead flow-step branch): `flowscript`/`flownode` jobs have
`runnable_path = <flow_path>/<step_id>`, which exact `= ANY(...)` never matched.
Split script vs flow triggerable paths; flow kinds now match the flow's own job
(bare path) and its step jobs via a `<flow_path>/%` prefix, bounded to declared
flows.
- P2 (test realism): the regression test now uses the production
component-prefixed triggerable key format (`<id>:script/...`), exercises a
flow-step-produced key, and asserts a viewer's own direct run of a declared
script stays denied (the P1 case).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): tie deployed-app S3 provenance to an app-origination marker (review)
Second CI-review round flagged that `permissioned_as` still does not prove a job
was app-launched: a runnable configured with its own `on_behalf_of` makes a direct
`/jobs/run` resolve `permissioned_as` to that identity (the app author), so a viewer
with run access could execute a declared runnable directly, craft an S3 result, and
read it back through the app. The flow-path `LIKE fp || '/%'` match also let `_`/`%`
in a declared path admit unrelated flows.
Introduce a real app-origination marker instead of inferring provenance:
- Add `JobTriggerKind::App`; `execute_component` stamps every app-launched job with
`trigger_kind = 'app'` + `trigger = <app path>`. A direct `/jobs/run` cannot set
this, so it is the authoritative signal that a file was produced *by the app*.
- The provenance gate's recent-production check collapses to
`trigger_kind = 'app' AND trigger = <this app path>` (+ the 3h window and result
containment). This drops the forgeable `created_by`/`permissioned_as`/
`runnable_path`/kind logic entirely and removes the `LIKE` wildcard issue.
- Provenance is scoped to THIS app's path, so another app's jobs (even same author)
do not authorize this app's reads.
Regression test rewritten to the marker model: an app-produced key clears for viewer
and admin; a direct run whose `permissioned_as` resolves to the author stays denied
(the forgery); another app's output stays denied. Adds `app` to the OpenAPI
JobTriggerKind enum.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(apps): assert execute_component stamps trigger_kind='app' at runtime
Adds an end-to-end test that runs a real script component through the app
runtime (`apps_u/execute_component`) and asserts the enqueued job carries the
app-origination marker `trigger_kind = 'app'` + `trigger = <app path>` (not the
runnable path). The provenance-gate tests seed the marker directly; this proves
the runtime actually produces the exact marker the gate depends on.
execute_component commits the job row and returns its id, so the assertion reads
the row directly — no worker needed to run the job.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(triggers): reject trigger_kind=app for suspended-job reassignment (review)
`JobTriggerKind::App` (added for the app-origination S3 marker) became a valid
value for the resume/cancel suspended-trigger routes, whose handler derives the
table name `<kind>_trigger`. There is no `app_trigger` table, so both endpoints
would fail with a missing-relation database error (500). Reject `App` in
`get_suspended_trigger` alongside webhook/schedule so it returns a clean 400.
Adds a regression test asserting the reassignment route returns 400 (not 500) for
trigger_kind=app.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): don't stamp app-origination marker on preview runs (review)
The app-origination marker (trigger_kind='app') was stamped unconditionally,
including preview mode. A preview lets a `jobs:run` caller supply arbitrary
`raw_code` against ANY app path without that app's deployed policy (raw_code with
no path/id skips all app authorization), so a preview returning
`{"s3":"<author-only-key>"}` would forge the exact marker the S3 provenance gate
trusts and read the victim app author's file.
Gate the marker on `!is_preview`: only deployed, policy-checked executions are
app-provenanced. Preview/editor S3 display does not rely on this marker (the editor
routes reads through the force_viewer allowlist), so nothing legitimate regresses.
Adds a regression test asserting a preview run's job is not stamped trigger_kind='app'.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): editor-authorize preview marker + per-viewer S3 provenance isolation (review)
Closes the codex P1 (preview forgery) without breaking editor preview downloads,
and adds cross-viewer isolation to the provenance gate.
- Preview marker now requires app write: `execute_component` stamps the
app-origination marker on a preview only when the caller can EDIT that app
(`require_is_writer`), instead of never stamping previews. An app editor already
wields the app's author identity (they can deploy a component that reads the same
file), so marking their own preview is no escalation and keeps preview-produced
S3 results downloadable in the editor; a `jobs:run`-only caller who cannot edit
the app still cannot forge the marker. Deployed runs are unchanged (always
marked).
- Per-viewer isolation: the provenance gate now also requires
`j.created_by = <this caller>`. The security boundary stays the un-forgeable
`trigger_kind='app'` marker; `created_by` is an additional filter ANDed under it,
so it only narrows — a viewer can only download keys their OWN app runs produced,
not another viewer's result. Restores the per-caller scoping #10048 had, now safe
on top of the marker.
Tests: preview marked iff caller can edit the app; cross-viewer isolation (another
viewer's app-marked key denied, no admin bypass); direct-run and other-app keys
still denied; deployed run still stamped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): require apps:write scope (not just writer ACL) to mark preview provenance (review)
require_is_writer checks the user's underlying ACL but ignores token scopes, so a
writer's token deliberately scoped to apps:run/apps:read/jobs:run but WITHOUT
apps:write could still mark a preview and forge provenance — even though that token
cannot deploy the app (update_app requires apps:write), breaking the "any marked
caller can deploy equivalent code" rationale.
Require BOTH apps:write:<path> scope (check_scopes) AND the writer ACL
(require_is_writer) before stamping a preview's app-origination marker. Deployed
runs unchanged.
Adds a scope-restricted-writer token to the test (apps:run/read + jobs:run, no
apps:write) and asserts its preview stays unmarked; retains the full-editor
positive case and the non-editor negative case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): never app-provenance preview runs; read editor S3 as the caller (review)
Simplifies the preview handling: a preview executes as the *caller* (Viewer mode),
never as the author, so its results must be read back as the caller — never
author-mode — and must never carry the app-origination marker. This removes the
whole `require_is_writer` / `apps:write` / `can_preserve_on_behalf_of` reasoning
(which was also unsound: a writer's token or session may not be able to deploy a
component running as the app's on-behalf identity, so marking their preview could
still escalate).
- Backend: mark the app-origination marker for deployed runs only (`!is_preview`).
- Frontend: `getS3File` (AppImage/AppPdf/AppDownload) now routes editor/preview
reads through the viewer-scoped `job_helpers/download_s3_file` endpoint (reads as
the caller), matching what DisplayResult/ParqetCsvTableRenderer already do; only
a deployed app view uses the provenance-gated `apps_u` endpoint. This is the path
that previously relied on marking previews, so nothing regresses.
Test: a preview is never app-provenanced (owner's own preview and a non-editor's
both stay unmarked). Cross-viewer isolation, deployed marking, and the reassignment
guard are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): app components run on-behalf of the app, not the referenced runnable (review)
Root-causes codex's on-behalf-preview finding: `execute_component` was overriding the
app's resolved on-behalf identity with the referenced script/flow's OWN
`on_behalf_of` (its `on_behalf_of_email`). That is wrong in the app context — the
app's execution mode should govern:
- A Viewer-mode app could execute a component AS the referenced runnable's on_behalf
identity (privilege confusion / escalation), instead of as the viewer.
- A preview would run as that identity rather than as the caller, so its S3 output
could not be read back as the caller — the download-identity mismatch codex flagged.
Always use the app-resolved identity (author in author-mode, caller in
viewer/preview); a referenced runnable's own `on_behalf_of` no longer leaks into app
execution. Direct `/jobs/run` still honors a runnable's `on_behalf_of` (unchanged).
With this, previews always run as the caller, so reading editor/preview S3 as the
caller (viewer-scoped `job_helpers`) is unconditionally correct.
- Test: the deployed-component e2e now seeds the script with a distinct on_behalf and
asserts the component job's `permissioned_as` is the app identity, not the script's.
- Also reword the getS3File `configuration` param comment to describe current state
only (AGENTS.md comment rule).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(apps): surface 'app' trigger kind in Runs UI; condense provenance comments (review)
Addresses codex review nits:
- Add `app` to `jobTriggerKinds`, `triggerIconMap` (LayoutDashboard), and
`triggerDisplayNamesMap` so app-component jobs (which now carry
`trigger_kind = 'app'`) are filterable in Runs and render their trigger info.
- Condense the app-origination marker, on-behalf-identity, and provenance-gate
comments to state each invariant once in <=4 lines at its relevant site
(AGENTS.md comment rule).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an opt-in `wm_content_transfer_encoding: "base64"` field to the composite
result. When set (together with `wm_content_type`), result_to_response decodes
the string result into raw bytes before sending it, so sync HTTP routes/webhooks
can return arbitrary binary payloads (PDFs, images, ...) with any content type —
not just as base64 text or via object storage.
Explicit and safe: the encoding is never guessed, invalid base64 is a hard error
(no silent fallback to the encoded text), an unsupported encoding is rejected,
and a transfer encoding without a content type is rejected. Existing string
responses are unchanged.
Closes#5986
* fix: replicate all secrets on fork with external backend (WIN-2161)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test: add Azure KV fork secret-replication reproduction (WIN-2161)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style: condense clone_variables invariant comment (WIN-2161)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test: drive real create_fork handler in Azure KV repro (WIN-2161)
Replace the windmill-common test that mirrored clone_variables' loop with an
end-to-end test in windmill-api-integration-tests that exercises the real
migration, create_fork and variable-read endpoints against a local Azure KV
emulator. Verified it fails (404 "not found in Azure Key Vault") without the
fix and passes with it; unique per-run ids keep it robust to the emulator's
persistent state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(sessions): sync AI-session editor preview with workspace edits + stop phantom autosave (WIN-2160)
Two related draft-sync fixes surfaced by the new AI sessions preview.
1. Session preview went stale after a workspace edit. A session's editor
runtime cell (content store + loadedPath) outlives the sessions page: it
survives toggling to workspace mode and MRU tab eviction. The shared
per-user draft can change while the editor is unmounted — most visibly by
editing the same item in the classic workspace editor, or from another
device — but on the next mount the load early-returns on the still-set
loadedPath and the preview keeps showing the pre-toggle content. Fix:
invalidate the cell's loadedPath when SessionEditorTarget unmounts, so the
next mount re-fetches the draft as a clean first load. This also sidesteps
a Monaco model-reuse race (a force-reload that remounts the editor while
the old one is still disposing renders a stale model) and prevents the
outbound draft-sync from posting the stale store back (ready() stays false
until the reload lands). Applies to all three editor kinds (script, flow,
raw app) since they share SessionEditorTarget.
2. Opening a deployed script in the full-page editor autosaved a phantom
draft with no user change. The deployed baseline carries a server-derived
assets: [] that the editor's draft value never reproduces, so
draftValuesEqual never matched baseline, discardIf returned false, and the
settle-time write posted a no-op draft. Fix: ignore assets in the
draft-vs-baseline comparison (it's derived from content, so it can't mask a
real change).
Fixes WIN-2160
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(sessions): condense teardown-invalidation comment to repo comment-length rule
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Windows integration-test build (`cargo test --all --features …`) fills
the runner's C: drive during linking. profile.dev leaves the (large)
windmill workspace crates at the default debug = 2, so full debug info is
emitted into every object file and embedded in each test binary — the
dominant consumer of the ~63GB free on the runner. The previous
split-debuginfo=off knob only suppressed the separate .pdb, leaving the
embedded debug info in place; it was borderline and the Rust 1.97.0 bump
(v1.755.0) pushed it over into a disk-full failure.
Set CARGO_PROFILE_DEV_DEBUG=0 and CARGO_PROFILE_TEST_DEBUG=0 so no debug
info is generated at all for the CI dev/test profiles. This supersedes
split-debuginfo=off (no debuginfo => no .pdb, no mspdbsrv type server) and
substantially shrinks the target dir. CI-only; local dev builds are
unaffected.
Fixes WIN-2162
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: add per-workspace job-retention override (EE)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 2ba6a2a75b6fc97858b306b2c98ada481e363c10
This commit updates the EE repository reference after PR #658 was merged in windmill-ee-private.
Previous ee-repo-ref: e7fb36acd813cd717bcf05f5aafbf81de271d618
New ee-repo-ref: 2ba6a2a75b6fc97858b306b2c98ada481e363c10
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix: clearer errors on auto-draft save failure (WIN-2157)
When an autosave draft save fails, the cloud indicator now surfaces the
backend reason on hover (native title tooltip) in addition to the
existing click popover, so the cause is discoverable without a click.
Backend now returns a clearer, actionable message:
- `require_can_write_path` distinguishes a malformed path (unrecognized
namespace prefix -> BadRequest) from a genuine permission denial, and
the deny message spells out where the user *can* write.
- `require_owner_of_path` no longer panics with an out-of-bounds index on
a malformed single-segment path (e.g. a bare `u`/`f`); it returns a
clear BadRequest instead. Covered by a regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: trim narrative comment to invariant in drafts.rs (WIN-2157)
Address CI review (AGENTS.md: comments record constraints, not narration,
≤4 lines): keep the malformed-path invariant, drop the motivation tail.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: don't let a malformed stored draft 400 the draft listing (WIN-2157)
Address CI review (P1): require_can_write_path can now return BadRequest
for a malformed path, and list_drafts propagated it — so a single
malformed stored draft row (the draft table has no path constraint;
legacy/admin-authored rows may be malformed) would make GET /drafts/list
return 400. Treat BadRequest like NotAuthorized there: the row is simply
not writable. Verified e2e on EE — listing returns 200 with can_write
false for the malformed rows.
Also trim "unchanged"/"still" drafting-history narration from the
regression test comments (P2, AGENTS.md).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: compress list_drafts comment to 4 lines (WIN-2157)
Address CI review P2: keep the constraint (draft table has no path
constraint) and the invariant (one malformed row must not 400 the
listing) within the AGENTS.md ≤4-line limit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): keep draft autosave alive after AI-session round-trip
A UserDraft entry is shared by refcount across the components editing the same draft — notably an AI-session preview and the nav editor on either side of the Workspace<->AI Sessions toggle. The entry's autosave mirror was a $effect.root created inside whichever component first acquired it; when that component (the session preview) unmounted while the returned-to nav editor still held a refcount, the mirror stopped firing even though the entry lived on — silently killing autosave in the workspace editor for scripts, flows and (raw) apps. Move the cell out of the mirror root (so handles survive) and re-home the mirror to each new acquirer, so it is always owned by a mounted component.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(frontend): record mirror-ownership invariant on releaseEntry
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): preserve sync baseline across mirror re-home
Addresses a re-home edge case (Codex review): the replacement mirror rearmed the first-write skip, so a draft edit the outgoing mirror had not yet observed (e.g. a session edit still pending at the Workspace<->AI Sessions handoff) was swallowed as the new baseline instead of POSTed, dropping the final change. Persist the serialization baseline on the entry (mirrorBaseline) and, on a re-home, seed the mirror from it without re-arming the skip — so a genuine unobserved change still syncs while an unchanged inherited value still doesn't POST.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): make draft autosave mirror component-independent
Replaces the re-home approach (Codex review): re-homing the mirror to the last acquirer assumed LIFO holder lifetimes, which the sessions UI breaks — it keeps multiple warm session previews mounted at once, so two warm previews of one draft share the entry and closing the newer one killed autosave in the surviving older one. Instead create the entry's mirror $effect.root in a microtask, where no component/effect is active, so it is a true top-level root owned by the ENTRY: it survives every holder unmounting and is disposed only at refcount 0. Removes the re-home/baseline bookkeeping entirely.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(frontend): condense mirror-deferral comment per review
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(apps): authorize deployed-app S3 reads on-behalf of the author for logged-in viewers
A logged-in user viewing a deployed app now reads S3 files (rich result,
table/image/PDF preview, CSV export, download, metadata) the same way an
anonymous viewer already does: on-behalf of the app author per the app policy's
execution_mode, gated by an app-provenance check — instead of against the
viewer's own S3 permissions. This aligns S3 with every other thing an app does
(scripts, flows, resources all already run on-behalf of the author) and lets an
operator who lacks folder S3 permission still see data rendered inside the app.
The raw job_helpers/* S3 API stays viewer-scoped: a viewer who lacks folder
permission is still denied there. Only which endpoint the app frontend uses for
logged-in deployed viewers changes.
Backend:
- Add app-scoped, provenance-gated apps_u/* variants for all S3 display ops
(download_s3_file already existed; add download_s3_parquet_file_as_csv,
load_file_metadata, load_file_preview, load_parquet_preview, load_csv_preview,
load_table_count). Each routes through one shared helper
(app_s3_on_behalf_and_provenance) that scope-confines an app embed token,
resolves the on-behalf identity, and runs the provenance gate ONCE before
dispatching to the EE *_internal S3 helpers.
- Close the confused-deputy hole in check_if_allowed_to_access_s3_file_from_app:
the unconditional Ok() bypass for a logged-in, non-embed session now only
applies in viewer execution mode (where the on-behalf identity IS the viewer,
so the viewer's own permissions still bound the read downstream). Author-mode
reads (anonymous/publisher) always enforce provenance, for anonymous and
logged-in viewers alike, so a viewer cannot launder the author's S3
permissions with an arbitrary file_key.
Frontend:
- Route the deployed-app view through apps_u/* using the app-viewer isEditor
signal instead of login state (the old $userStore proxy wrongly sent
logged-in deployed viewers to the viewer-scoped job_helpers API). Editor and
preview keep viewer identity via job_helpers.
execution_mode: viewer remains the escape hatch for per-viewer S3 enforcement.
Fixes provenance-gated S3 display for logged-in operators on deployed apps.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(backend): document cargo features, restarting the dev backend, and filesystem object storage
The dev backend runs `cargo watch --features quickjs` by default, which omits S3,
EE, MCP, and non-JS runtimes — feature-gated routes then 404 or return a
"requires <feature>" stub at runtime. Add a backend/CLAUDE.md section that:
- explains that you must restart the backend with the appropriate features to
exercise gated functionality, with the pid/cwd-scoped restart recipe (never
pkill target/debug/windmill) and the PORT=$BACKEND_PORT gotcha;
- documents what each commonly-toggled feature gate does (private, enterprise,
license, parquet, duckdb, language runtimes, mcp, trigger kinds, no_auth) plus
common combinations;
- documents using the built-in FilesystemStorage large-file storage for dev
workspace object storage (hidden from the UI dropdown; set via
edit_large_file_storage_config), including the advanced_permissions shape.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): don't flatten inner query in app-scoped S3 preview routes
axum's `Query` uses `serde_urlencoded`, which cannot deserialize the typed
(numeric/bool) fields of a `#[serde(flatten)]`-ed struct and 400s on `limit` /
`offset` ("invalid type: string, expected u32"). The app-scoped
load_csv_preview / load_parquet_preview / load_table_count routes flattened
LoadPreviewQuery / LoadCountQuery, so their previews were broken. Restate the
fields directly on the outer query structs (with an into_inner() to rebuild the
inner query) and extend the CE OSS stub to match.
Also bumps ee-repo-ref.txt for the companion EE csv-separator panic fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: address CI review — nested DisplayResult routing, byte-range contract, docs, tests
- [P1] Thread `appPath` into the nested `DisplayResult`s (render_all children and
the expanded-result drawer) so logged-in deployed viewers route nested/expanded
S3 tables, images, PDFs, and downloads through `apps_u/*` too, not job_helpers.
- [P2] Mark `read_bytes_from`/`read_bytes_length` required on the
`apps_u/load_file_preview` route (they are non-optional in LoadFilePreviewQuery),
and mirror the full query shape in the CE OSS stub so the byte-range contract is
enforced identically on CE and EE.
- [P2] Fix the backend retrigger command in backend/CLAUDE.md: cargo watch runs
from `backend/`, so `touch README.md` (not `backend/README.md`).
- [P2] Trim app_s3_onbehalf.rs comments per AGENTS.md (state the invariant once,
no drafting-history narration).
- Extend the integration test to cover the table-count, csv-preview (numeric
limit/offset deserialization), and file-preview (byte-range required) routes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(apps): tighten S3 provenance-gate comments per AGENTS.md
Consolidate the viewer-mode / author-mode rationale to ≤4 lines at each branch
of the gate, and drop the repeated explanation from the shared
app_s3_on_behalf_and_provenance doc comment (which now just states what the
helper does). No behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to f292a1040da6a667ce7c22abf63ec0debfdd480f
This commit updates the EE repository reference after PR #657 was merged in windmill-ee-private.
Previous ee-repo-ref: a582389084eb363997cb5e8053f29220e0d3eaec
New ee-repo-ref: f292a1040da6a667ce7c22abf63ec0debfdd480f
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat: add local-review-codex skill and bump CI codex to gpt-5.6-sol
Add a `/local-review-codex` skill that runs the same Codex review as the
codex-pr-review GitHub action, locally and scoped to unpushed work
(committed + uncommitted), so contributors can catch what CI would flag
before pushing. Same REVIEW.md policy, gpt-5.6-sol model, and xhigh
reasoning effort as CI; runs read-only so it cannot modify the tree.
Also bump the CI codex-pr-review job to model gpt-5.6-sol on Codex CLI
0.144.1 (from gpt-5.5 / 0.128.0), and document the new skill in AGENTS.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(local-review-codex): use bash in docs and fall back to origin/main
Address CI review findings:
- Docs invoked the runner with `sh`, which ignores the Bash shebang and
fails on `set -o pipefail` under Dash (/bin/sh on Debian/Ubuntu). Use
`bash` and note it in SKILL.md.
- Default base `main` is unresolved in checkouts that only have
`origin/main`; resolve through a local ref first, then fall back to the
remote-tracking ref. Fix the misleading `git fetch` recovery hint.
- Pin the codex-not-found install hint to @0.144.1 to match the workflow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`sign_s3_objects` minted a long-lived HMAC bearer signature for any S3 key
handed to it, by any authenticated workspace member, with no check that the
caller was allowed to read that key. Since `validate_s3_signature` only
verifies the HMAC and expiry at fetch time, any member (operators included)
could mint a transferable capability to read arbitrary S3 keys, bypassing the
advanced S3 permission rules (`check_lfs_object_path_permissions`).
Authorize the read at mint time: add an `ApiAuthed` extractor and, before
signing each key, require the caller's own `S3Permission::READ` via
`get_workspace_s3_resource_and_check_paths`. A caller can no longer sign a key
they cannot themselves read. The fetch-side validators are left unchanged.
The only legitimate caller is the wmill SDK invoked from an app-author job,
whose token authenticates as the executing (author) identity — which can read
the key — so authorized app display is unaffected.
Adds an integration test proving an authorized caller can sign a readable key
(and the signature validates end-to-end through the presigned fetch route)
while an unauthorized caller is refused.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: keep agent-worker server job-completed processor alive on init-script failure
The agent-worker API server's background job-completed processors relay
completions on behalf of many remote agent workers. The processor loop exited
(dropping its receiver) on an init-script failure, but on the server that failed
init script belongs to a remote worker, not the server. Once enough processors
exited, the shared completion channel disconnected and every /send_result POST
returned 500, stranding completions and creating zombie-job restart loops.
Add an is_agent_server flag so server relay processors don't self-terminate on
init-script failure. Pins the EE companion change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: bump EE ref for send_result wait-for-processor change
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: agent-worker server survives a failed init script
End-to-end regression for the agent-worker-server processor bug: an agent worker
runs a failing init script, POSTs the failed init-script completion to
/send_result, and the test asserts the server's background job-completed
processor stays alive (a subsequent job completes and no bg-processor critical
alert is raised). Fails if the is_agent_server guard is removed (the processor
breaks, the supervisor raises a critical alert).
Requires --features enterprise,license,private,agent_worker_server.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: replace heavyweight init-script e2e with focused unit tests
The panic/respawn/alert and 503 timeout paths are now covered by fast, deterministic
unit tests in windmill-api-agent-workers (supervise_processor, classify_send). Drop
the enterprise-only, global-config-mutating e2e in favor of those. Bump EE ref.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to bc45d9275d4307132927dc8ad3e82049b1aed463
This commit updates the EE repository reference after PR #653 was merged in windmill-ee-private.
Previous ee-repo-ref: 2aca03f28bb37e938ae548b81f1620b2e00dc0f7
New ee-repo-ref: bc45d9275d4307132927dc8ad3e82049b1aed463
Automated by sync-ee-ref workflow.
* chore: bump EE ref for bg-processor alert rate-limiting
Picks up windmill-ee-private#654: exponential backoff + rate-limited critical
alerts in supervise_processor, addressing the code-review nit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to d48c0e01e8601a372353c032dd237ddb6fa3bbad
This commit updates the EE repository reference after PR #654 was merged in windmill-ee-private.
Previous ee-repo-ref: f89eeb6e333614850ef650e7df78e3c2335f107c
New ee-repo-ref: d48c0e01e8601a372353c032dd237ddb6fa3bbad
Automated by sync-ee-ref workflow.
* chore: bump EE ref for graceful-shutdown-during-backoff fix
Picks up windmill-ee-private#655: supervise_processor re-checks shutdown before
respawn and selects on the shutdown broadcast during backoff, so a crash-loop
backoff can't hang graceful shutdown. Addresses the Codex P1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 9bc5dfb9ce73a2d9b981a1de86eea6aa26688b79
This commit updates the EE repository reference after PR #655 was merged in windmill-ee-private.
Previous ee-repo-ref: 8dc3b3d9ec8f9c28b227d36c2a1327b4b2017665
New ee-repo-ref: 9bc5dfb9ce73a2d9b981a1de86eea6aa26688b79
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>
Update the pinned Rust toolchain from 1.93.0 to 1.97.0 (latest stable,
released 2026-07-09) across the worker/server build Dockerfiles
(Dockerfile, docker/DockerfileFull, docker/DockerfileFullEe) and all CI
workflows that pin a toolchain.
Verified the backend compiles cleanly with 1.97.0 under `-D warnings`
(the default RUSTFLAGS used by actions-rust-lang/setup-rust-toolchain).
Fixes WIN-2155
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(mcp): add multi-workspace MCP tokens via the gateway endpoint
A single MCP token with no bound workspace (workspace_id NULL + mcp scope)
now works across every workspace the token owner can access, served through
the existing /api/mcp/gateway endpoint. This avoids having to register one
MCP server entry per workspace in clients like Claude/Cursor.
In multi-workspace mode the runner exposes a synthetic `list_workspaces`
tool plus the generic API endpoint tools, each workspace-scoped one gaining
a required `workspace_id` argument (mirroring the proxy pattern users built
externally). Per-workspace scripts/flows are not enumerated to avoid
flooding the tool list — they are run via runScriptByPath/runFlowByPath
with an explicit workspace_id.
Auth is resolved per tool call: the gateway middleware detects a
workspace-less mcp token and marks the request MultiWorkspaceMcp, and the
runner resolves a per-workspace ApiAuthed from the raw token via the
AuthCache (validating membership; superadmins may act in any workspace).
Single-workspace tokens are unchanged.
Frontend: the MCP token creation flow gains an "All workspaces" option that
produces a workspace-less token and the gateway URL.
Fixes WIN-2153
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(mcp): cover multi-workspace endpoint tool transformation
Unit tests for endpoint_tool_to_mcp_tool_multi and list_workspaces_tool:
workspace-scoped tools gain a required workspace_id arg, global tools are
left unchanged, workspace_id is not duplicated, and list_workspaces takes
no arguments.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(mcp): forward script/flow args for runScriptByPath/runFlowByPath
These endpoints have an additionalProperties body (no declared properties),
so build_request_body previously returned an empty body and dropped every
script/flow argument. This was latent for the per-path run endpoints and
became load-bearing in multi-workspace mode, where scripts/flows can only be
run via runScriptByPath/runFlowByPath — parameterized runs silently lost
their arguments.
build_request_body now forwards all arguments not consumed by a path/query
parameter for pass-through (additionalProperties) bodies, keeping the strict
declared-only behavior for endpoints with explicit properties. The runner
strips the synthetic workspace_id argument before dispatch so it can't leak
into the forwarded body.
Reported by Codex review on #10043.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(mcp): note workspace_id requirement in multi-workspace tool descriptions
Workspace-scoped tools already gain a required workspace_id parameter (with
its own schema description) in multi-workspace mode, but the tool's prose
description was unchanged. Append a note so models/clients that read the
description text know to pass workspace_id (and to call list_workspaces
first). Global tool descriptions are left untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(mcp): trim multi-workspace tool/arg descriptions
The workspace_id note repeats across every workspace-scoped tool in each
tools/list, so keep it terse: description suffix "Requires `workspace_id`."
and arg description "Target workspace id (from list_workspaces)." to avoid
spending tokens on repeated boilerplate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(mcp): enforce script/flow path scopes for multi-workspace run-by-path
In multi-workspace mode runScriptByPath/runFlowByPath are the only way to run
scripts/flows, but they were authorized against the endpoint scope only — never
the caller's mcp:scripts:/mcp:flows: path scopes. A granular token could run
items outside its allowed paths (e.g. mcp:scripts:f/team/* + mcp:endpoints:*
running f/other/secret), and a mcp:endpoints:* token could run arbitrary
scripts.
Now these two endpoints are authorized by the script/flow scope of the
requested path (matching single-workspace mode's per-item tools): exposed in
list_tools only when the token grants some script/flow (McpScopeConfig::has_any),
and at call time the path is checked via is_allowed("script"/"flow", path).
Verified e2e: mcp:scripts:f/team/* runs f/team/* but is denied f/other/*;
mcp:endpoints:* alone no longer exposes or runs run-by-path.
Reported by Codex + Pi review on #10043.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(mcp): deny run-by-path for mcp:favorites multi-workspace tokens
mcp:favorites sets granular=false, so the previous run-by-path scope check
(gated on `granular`) was skipped entirely — a default "Favorites only"
all-workspaces token could run any script/flow by naming its path, bypassing
the favorites restriction.
Favorites are an enumerated set reachable only through per-item tools, not by
arbitrary path, so they grant nothing for run-by-path. has_any() now returns
true only for mcp:all (not favorites), and the call-time check drops the
`granular` gate and relies on is_allowed() directly (already false for
favorites, true for mcp:all, pattern-matched for granular).
Verified e2e: mcp:favorites no longer exposes or runs run-by-path; mcp:all
still runs; granular script scopes still path-enforced.
Reported by Codex review on #10043.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): show nested restart button for subflows nested in containers
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): keep nested-restart flat fallback anchored to the leaf
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): show optimistic user message and fork-creation label before beforeSend
In AI chat, `sendRequest` previously set `loading` and pushed the user
message only after the `beforeSend()` hook completed. For forked sessions
`beforeSend` runs several sequential API calls (materialize session, flush
files, create workspace fork, load copilot config) that take seconds, while
the composer clears its textarea immediately. The result: the message text
vanished into a void with no bubble and no loading indicator until the fork
finished.
Now the user bubble and loading indicator are shown optimistically before
`beforeSend`, with context elements and the snapshot attached afterwards. A
general-purpose `loadingLabel` lets any `beforeSend` hook describe its
pre-flight work; the session hook sets "Creating workspace fork..." around
`commitSessionWorkspace`. If `beforeSend` throws, the optimistic bubble and
loading state are rolled back.
Fixes WIN-2150
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): make Stop/Escape cancel the send during the beforeSend pre-flight
Showing the loading indicator before beforeSend also exposed the Stop
button and Escape handler during "Creating workspace fork...", but the
abort controller was created after beforeSend, so cancel() had nothing to
abort and the request still fired once the pre-flight resolved.
Create the abort controller before beforeSend and check `signal.aborted`
after it: a Stop/Escape during the pre-flight now rolls back the optimistic
turn and skips the request. Factor the rollback into a shared helper reused
by the beforeSend-failure and cancel paths, and refresh the now-stale
beforeSend doc comment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): restore prompt and hand off queued message on pre-flight cancel
The pre-flight abort check rolled back the optimistic turn and returned
early, skipping the recovery the main cancel path runs. Because the input
clears its composer on send, a Stop/Escape during "Creating workspace
fork..." lost the typed prompt from both the bubble and the composer, and
bypassed the queued-message handoff.
Mirror the main "cancelled before usable output" path: restore the prompt
to the composer via the same restoreInstructions helper, or auto-send a
queued message when one is taking over, and return true so a parent
queued-flush doesn't re-queue the cancelled turn.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: scope flow script-edit drawer to session workspace and fix scroll
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: scope flow schema inference to session workspace
Thread an optional workspace through loadSchemaFromPath/loadSchemaFlow/
loadSchemaFromModule/loadFlowModuleState/initFlowState/pickScript/pickFlow
and pass the op (session) workspace at fork-context call sites, so a flow
opened in an AI session resolves path-referenced scripts/subflows against
the session workspace instead of the nav workspace.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: scope script editor log panel and git-repo pickers to op workspace
LogPanel and the ansible git-repo viewer/picker read the nav workspace
directly; pass the script editor's op workspace so past-test results/logs
and git-repo resource/file lookups target the session workspace in a fork.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: correct session pipeline trigger-editor workspace comment
The comment claimed session activation syncs $workspaceStore; SessionPicker
intentionally does not, so trigger create/edit/delete from a fork session's
pipeline canvas writes to the nav workspace. Document the known limitation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: forward op workspace to git-repo S3 file browser
GitRepoViewer scoped its own calls to the op workspace but rendered the
nested S3FilePickerInner without workspace={ws}, so the file list/preview/
metadata still queried the nav workspace with a session-workspace prefix.
Addresses Codex review on #10025.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): open new script/flow/app in AI session without "not found"
"Open in AI session" on a never-deployed item opened the session preview
against the friendly live-edited path (script.path / $pathStore) instead of
the URL draft path the editor loads and saves by, so get-by-path 404'd. It
also flushed only queued autosaves, so an untouched new item — which never
triggered autosave — had no draft row to load at all.
Target the URL draft path (userDraftPath / liveEditorDraftStoragePath;
raw-app already used appPath), and add UserDraft.forcePersist to materialize
a brand-new draft in beforeOpen, gated to never-deployed items where there is
no deployed baseline to discard against.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): label a new session preview tab by its friendly name
A never-deployed item's preview tab read draft_<uuid> instead of the typed/
auto name. The sessions page can't reactively read a runtime cell's state
across reactive roots, so the live editor (SessionEditorTarget, handed the
runtime as a prop) now stamps a transient friendlyLabel onto the tab model —
which the page does observe — via a pure draftFriendlyLeaf helper. Unifies
scripts, flows and raw apps through one path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): address review nits on AI-session open
- Flow drawer (FlowEditorDrawer) mounts FlowBuilder with no
liveEditorDraftStoragePath, so gating the AI button solely on it hid the
session entry point there; fall back to $pathStore (the pre-PR behavior for
those deployed-flow drawers) while the main editor still prefers the URL
draft path.
- Clear a tab's stamped friendlyLabel when it is retargeted, so a draft tab's
friendly name no longer lingers after navigating to a plain page.
- Trim the repeated persist-hook comments to satisfy the AGENTS.md comment rule.
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: Ruben Fiszel <ruben@windmill.dev>
Perpetual scripts (restart_unless_cancelled) re-pushed their restart job
with custom_timeout = None, so every rerun ignored the script's
configured timeout and fell back to the instance-level job_default_timeout.
Only the first run honored the script timeout.
Fetch the script timeout alongside restart_unless_cancelled (both cached
by the immutable script hash) and pass it as custom_timeout when
re-pushing the perpetual job.
Fixes WIN-2149
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `// sandbox` annotation (and global nsjail sandboxing) restricted Deno to
`--allow-run=git,/usr/bin/chromium`. Both binaries can be coerced into spawning
`/bin/sh`, escaping Deno's permission model:
- git via hook configs, e.g. `git -c core.fsmonitor='/bin/sh -c <cmd>' status`
- chromium via subprocess-launcher flags, e.g. `--renderer-cmd-prefix` /
`--gpu-launcher`, pointed at a launcher the script writes into `./`
Because the subprocess is spawned by git/chromium — not Deno — it is invisible
to Deno's permission checks, giving any user with script-execution permission
arbitrary OS command execution (root, in the default worker container).
Critically, the Deno runtime is the ONE language never wrapped in nsjail (there
is no run.deno.config.proto; every other language has one). So for deno the Deno
permission model is the *entire* sandbox — there is no OS-level containment to
fall back on, and handing it any subprocess-spawning binary is an unconditional
escape regardless of the nsjail setting.
Fix: emit no `--allow-run` in the restricted path, denying all subprocess
execution. The advisory's alternative (inject `-c core.fsmonitor=false ...`)
doesn't apply — the user controls the git/chromium argv, so any injected
hardening is overridden. Admins who accept the risk (e.g. puppeteer) can still
re-add specific binaries via `DENO_FLAGS`.
Verified with both PoCs on a running worker: git and chromium invocations now
return `Requires run access to "<bin>"`; the sandbox escapes are closed.
Fixes WIN-2151
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `// sandbox` annotation restricts Deno to `--allow-run=git,/usr/bin/chromium`.
git can be coerced into spawning `/bin/sh` via hook configs such as
`git -c core.fsmonitor=<cmd> status`, and that subprocess is spawned by git —
not Deno — so it is invisible to Deno's permission model. This let any user
with script-execution permission run arbitrary OS commands as root inside the
worker, fully defeating the sandbox.
The advisory's alternative (injecting `-c core.fsmonitor=false -c
core.hooksPath=/dev/null`) does not apply here: the user's own script invokes
git directly via `Deno.Command`, so Windmill cannot inject hardening flags into
that call. Removing git from the allowlist is the only complete fix. git was
originally allowed for git-sync-adjacent use, which no longer needs it.
Verified with the advisory PoC: git invocation now returns
`Requires run access to "git"` and the sandbox escape is closed. chromium
(puppeteer) support is preserved.
Fixes WIN-2151
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: accept bunnative language in AI chat flow step validation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: regenerate copilot flow schema from openflow spec
Run gen_openflow_schema.sh + minifiedOpenflowJson.sh instead of hand-patching. Also syncs three fields the checked-in generated files had drifted from since the last regen (reasoning_effort, reasoning_token_delta streaming event, aiagent tag).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: regenerate system prompts for bunnative openflow schema
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): name the draft in AI chat test-run confirmation
The confirmation card shown before an AI-chat test run displayed a
static, generic header ("Run script test"). Make it name the target
and clarify it runs the user's draft.
- Tool.confirmationMessage now accepts a function of the parsed args;
shared.ts resolves it before setting the tool status.
- test_run_script/flow/step (global chat) build a dynamic header
naming the script/flow/step, e.g. "Run a test of your draft of X".
- In-editor script/flow test-run tools say "Run a test of your draft".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): fall back to tool name in YOLO tooltip for function messages
The auto-accept ("bypassed in current mode") tooltip rendered
confirmationMessage directly. Now that it can be a function of the call
args, render the tool name instead of the function source there.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): use neutral wording in test-run confirmations
The global test-run tools fall back to deployed content when no draft
exists, so "your draft" could contradict what actually runs. Drop the
draft claim and just name the target: "Run a test of X".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: populate fork base picker for superadmin visiting a non-member workspace
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: resolve fork family for superadmin across sidebar picker and scope header
Extract the superadmin-visited-workspace fallback into a shared useForkableWorkspaces composable and apply it to WorkspaceFamilyPicker and WorkspaceScopeHeader so the sidebar fork picker and its fork-count trigger resolve the family for a superadmin viewing a non-member workspace.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: resolve superadmin-visited workspace name in the scope trigger chip
The sidebar scope trigger next to the fork picker read $userWorkspaces directly, so a superadmin viewing a non-member workspace saw its raw id instead of the resolved name/family. Thread the folded-in forkable list into WorkspaceScopeTrigger, and trim the now-duplicated per-site rationale comments to a pointer at the composable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): persist forked "Copy of X" script drafts
Forking a script ("Copy of X"), hub-forking, or seeding a new draft from
a URL/YAML/JSON import opened the editor with pre-filled content, but the
saved draft never appeared in the scripts list.
The edit route suspends autosave for every `?new_draft=true` load
(`UserDraft.stopSync`) so the seed write doesn't post as the user's first
edit, expecting ScriptBuilder to lift it. ScriptBuilder's `restartSync`
only ran inside `if (script.content == '')`, so a non-empty seed (fork /
hub / import) skipped it and left autosave suspended for the session —
both autosave and explicit Ctrl+S then silently no-op'd, so the draft was
never written and never listed.
Add an `else if` branch for pre-filled `new_draft` seeds that runs the
same stores-gated restart cascade (restart only, no template seeding),
restoring parity with the empty-new-script flow. The empty-seed block is
unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: condense scheduleRestartSync comments to ≤4 lines
Address Codex review P2: trim the helper and new-branch comments to the
core invariant per AGENTS.md's comment-length rule.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: show pending friendly path in new raw app session tab
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: reveal a subtle rounded grabber on the sessions chat/preview splitter on hover
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: cap session diff blocks so each item's card fits the drawer viewport
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: floor flow diff cap at its min height and tidy diff/splitter comments
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): scope raw-app/flow/script editors to the session workspace
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): scope flow and script editor operations to the session workspace
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): scope flow preview, inline-script creation and datatable schema to the session workspace
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): address Codex review — thread session workspace through flow resource pickers, script fetch, preview cancel/recording and path collision check
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): address Claude review — pass session workspace to preview FlowStatusViewer and align FlowChatManager guards
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): address Pi review — show acting workspace in script-not-found message and fetch picked script from it in EditorBar
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): address Codex review round 2 — thread session workspace into flow step test, raw-app inline runnable, inline editor toolbars and MCP OAuth path
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): address Codex review round 3 — thread session workspace into dynamic-input helpers and the flow-preview argument side panel (history/saved-inputs/captures)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): address Codex review round 4 — thread session workspace into nested flow/script drawers, flow chat inputs and the flow input side tabs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): address Codex review round 5 — thread session workspace into script-module fork/reload and key the raw-app schema cache by workspace
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): address Codex review round 6 — key the DB manager schema cache by acting workspace
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): address Codex review round 7 — thread session workspace into resource-valued arg pickers and the editor variable/resource helper drawers
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): scope the flow asset explorer's ResourceEditorDrawer to the acting workspace
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: thread acting workspace through flow asset explore controls
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: thread acting workspace through SQL REPL, secret args, helper forms, S3 inputs, saved inputs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(db-health): add connection sizing guidance
The Database Connections panel showed current/max connections but gave no
guidance on how to size max_connections for the deployment. Derive an estimate
from the live worker fleet: each worker instance shares a pool sized
DEFAULT_MAX_CONNECTIONS_WORKER + (workers - 1), and each server opens up to
DEFAULT_MAX_CONNECTIONS_SERVER (both overridable via DATABASE_CONNECTIONS).
The endpoint now returns live worker/instance counts, the default per-server
and per-worker pool sizes, the estimated peak worker connections, the reserved
superuser connections, and a recommended max_connections floor (workers + one
server + 25% headroom). Servers do not ping worker_ping, so the recommendation
assumes one server and exposes the per-server increment. The panel renders this
as a sizing breakdown and warns when max_connections is below the recommended
floor.
Fixes WIN-2147
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(db-health): single source for pool-size constants + sizing tests
Address review: db_connect.rs kept its own copies of DEFAULT_MAX_CONNECTIONS_*
that duplicate the windmill_common constants the sizing guidance reads, so
tuning the runtime pool size would silently leave the guidance stale. Re-export
the windmill_common constants from db_connect.rs so there is one source of truth.
Add unit tests for compute_connection_sizing covering the zero-fleet, single
worker, multi-instance, and reserved-clamp cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(db-health): 20% headroom and 200-connection minimum floor
Lower the sizing headroom from 25% to 20% and never recommend below 200
connections (postgres defaults to 100; cheap headroom for growth/bursts/psql).
Update the guidance message and unit tests accordingly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(db-health): honor DATABASE_CONNECTIONS in sizing recommendation
Address Codex P1: the runtime caps every process's pool at DATABASE_CONNECTIONS
when set (db_connect.rs), but the sizing guidance always used the default 50/5
pools. For a tuned deployment this under-estimated worker demand and could hide
a genuine under-provisioning (e.g. DATABASE_CONNECTIONS=100 with 5 instances is
500 worker connections, not 25).
compute_connection_sizing now takes the effective DATABASE_CONNECTIONS override
(read the same way db_connect.rs reads it): when set, each worker instance and
server pool is that value and the worker estimate is override * instances. The
response exposes server_pool_size / worker_pool_size (effective) and
database_connections_override; the panel renders both pool rows and labels them
(default) vs (DATABASE_CONNECTIONS), and the message states which source is used.
Adds a unit test for the override path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(db-health): exclude agent workers from connection sizing
Agent workers reach the API over HTTP (MODE=agent, Connection::Http) and hold
no postgres pool, but their pings still land in worker_ping (written server-side
by /api/agent_workers/update_ping). Counting them inflated the connection
estimate. Filter the fleet query by the worker-name prefixes: DB-connected
workers use "wk-" (WORKER_NAME_PREFIX), agent workers use "ag-"
(AGENT_WORKER_NAME_PREFIX). Only wk- workers/instances feed the estimate; ag-
workers are counted separately and surfaced as context ("N agent workers
excluded — they use HTTP, not postgres connections"). Adds a unit test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(cli): clarify workspace fork naming and parent-workspace context
Expand `wmill workspace fork`'s help and interactive prompts so the two
positional arguments are self-explanatory:
- Command description now explains that the fork is created from the
currently active (parent) workspace, that `workspace_name` is a
friendly display name that may contain spaces (quote it), and that
`workspace_id` is a bare slug auto-prefixed with `wm-fork-` which also
determines the git branch name.
- Interactive name/id prompts reworded to match.
Regenerated system_prompts CLI guidance to reflect the new description.
Fixes WIN-2148
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): default fork name to "<parent>'s fork", make it optional
The fork's display name is no longer effectively required — it now
defaults to "<parent workspace name>'s fork" (fetched via
get_workspace_name, falling back to the local profile name / id) and
stays fully overridable via the positional argument or interactive
prompt.
To produce this default, `setClient` and the parent-name lookup are
moved ahead of the name/id resolution. The id default is decoupled from
the possessive display name: when auto-naming, the id/branch slug is
derived from "<parent>-fork" (e.g. wm-fork-acme-fork) rather than the
awkward "<parent>-s-fork". Branch-rename forks keep their branch-derived
id.
Regenerated system_prompts CLI guidance.
Fixes WIN-2148
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(cli): fix workspace fork help — parent is branch-resolved, not active profile
Addresses the codex/pi review: the fork help said the parent is the
"currently active" workspace and told users to `wmill workspace switch`,
but createWorkspaceFork resolves the parent from the current git branch's
wmill.yaml mapping (tryResolveBranchWorkspace) and ignores the active
profile. Reword to describe the actual branch-based resolution.
Regenerated system_prompts CLI guidance.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(cli): note branch-derived fork id default in rename workflows
Addresses the codex review: the `[workspace_id]` help and the interactive
prompt said the default id is derived from the name, but rename workflows
(non-base branch / --from-branch) keep the branch-derived default
(`branchDefaultId ?? branchToForkId(idBasis)`) to keep the id/branch
aligned with the branch being converted. Document that special case
rather than changing the intentional behavior.
Regenerated system_prompts CLI guidance.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): cap auto fork name at 50 chars for long parent names
Addresses the codex review: the "<parent>'s fork" default appended
"'s fork" to a parent name that can itself be up to 50 chars (varchar(50)),
so a parent name over 43 chars produced a default exceeding the limit and
tripped the effectiveName.length > 50 guard — failing `wmill workspace
fork --yes` (or accepting the interactive default) for a valid parent.
Truncate the parent portion so the generated default stays within 50.
Verified end-to-end: a 48-char parent name now yields a 49-char default
("... Team's fork") and the fork is created successfully.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: preserve worker group tag override on 'Run again'
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: keep tag override in sharable hash on args change
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: disambiguate reserved __tag hash key from args named __tag
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: prefix carried tag in sharable hash and react to tag changes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: re-resolve dynamic tags on 'Run again' with an explanatory note
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: treat only $args-templated tags as dynamic on 'Run again'
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: let a carried tag coexist with an arg named __tag via duplicate keys
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* perf(sessions): scope preview-tab refresh to items a chat tool touched
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(sessions): drop dead editor pane, scope raw-app reload by path
Multi-target migration P0. SessionWrapper's inline editor pane was dead (the sessions page always mounts it with hideEditor); remove it and the single-target machinery (setSessionTarget/pickEditorTarget/target-keyed editor views). Scope the raw-app file/runnable preview reload to args.path (the app's workspace path) instead of the session target.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(sessions): back editor state with per-(kind,path) cells
Multi-target migration P1. Replace the three per-kind singleton stores/slots with per-(kind,path) cell maps, created on demand and kept (eviction deferred to P3). The runtime's public interface is unchanged: the flowStore/scriptStore/savedScript/rawApp/... getters and slot(kind) now forward to the 'active cell' per kind (a single-target shim, tracked by activePath, removed in P2 when the UI mounts one editor per tab). loadFlow/loadScript/loadRawApp and syncPreviewWithDeployed operate on the resolved cell; load logic and semantics are otherwise unchanged, so loading one item no longer clobbers another's state.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sessions): mount every editable preview tab as its own live editor
Multi-target migration P2 — the behavioral flip. resolvePreviewTab no longer takes a target: any editable route (script/flow/raw_app) resolves to an in-process editor, so several items are live at once (iframes remain only for real pages and regular non-raw apps). Each editor binds its own per-(kind,path) cell; the draft codecs close over that cell's store so two editors never cross-write. The single-target shim (activePath + the flowStore/scriptStore/... getters + slot(kind)) is removed; runtime exposes flowCell/scriptCell/rawAppCell(path). Tab open/navigate dedupe by (kind,path) and no longer setTarget. setLiveEditorDraft is gated on the visible tab (isActiveTab) so N editors don't clobber the one-per-(workspace,kind) live-draft slot (path re-key deferred to P4).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(sessions): evict unreferenced editor cells; drop dead warm-editor LRU
Multi-target migration P3. Bound the per-(kind,path) editor cell maps: pruneEditorCells drops every cell no open preview tab still references, wired to a new onTabsChanged adapter callback fired on each tab-set change — so closing or navigating a tab away from an item reclaims its cell (dedupe keeps <=1 editor tab per item, so a pruned item has no live editor to strand). Also remove the now-dead editorWarmIds/promoteEditorWarm/MAX_WARM_EDITORS warm-editor LRU: its only reader (SessionWrapper.mountEditor) was removed in P0, and mounted editors are already capped per-tab by mountedTabKeys.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(sessions): retire session.target; preview is fully tab-driven
Multi-target migration P4 (final). Remove the session.target field and setSessionTarget: the preview is driven entirely by the tab model now (P2). hydratePreviewTabs no longer seeds a tab from target (saved previewTabs only); openEditorInSession seeds the preview via resetSessionPreviewTabs; normalizeLegacySession drops the retired target field from old records. The setLiveEditorDraft focus gate (isActiveTab, one-per-(workspace,kind)) is kept as-is; a per-path re-key is a possible future refinement, not needed for correctness.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(sessions): describe editor cells as-is, not by their refactor history
Address standards review: AGENTS.md requires comments describe the code as it is, not its drafting history. Drop the 'used to be per-kind singletons' / 'pre-refactor empty editor' / 'now' phrasings from the cell comments.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(sessions): update stale runtime.rawApp.val comments to cell.store
Address spec review: two comments still referenced the removed runtime.rawApp.val accessor; the live code uses the per-cell store now.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(sessions): fix editor-cell comments after main merge
Main's #9993 added svelte-ignore comments describing the old
runtime.savedFlow.val / runtime.rawApp.val singleton bindings. The
multi-target refactor binds each tab's own editor cell (cell.store /
cell.saved), so update the comment text to match; the ownership_invalid_binding
directives themselves remain correct (the targets are still runtime-owned).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sessions): restore data-pipeline preview as a live editor tab
The multi-target refactor removed the old single-target editor pane —
PipelineEditorView's only mount point — so open_preview(kind="pipeline")
opened nothing, even though the chat tool and system prompt still make it
the first step of pipeline authoring.
Route a /pipeline/<folder> preview tab to the in-process graph editor:
- previewRouter: parsePipelineRoute + resolvePreviewTab map the folder to a
pipeline editor slot; PreviewSlot.editorKind gains 'pipeline'.
- previewTargetForSessionTarget('pipeline') returns the folder route target
(was undefined); open() keeps a single pipeline tab and retargets it to the
requested folder, since all pipeline tabs share one runtime.pipelineEditorState.
- PreviewTabHost mounts PipelineEditorView for the pipeline slot.
- PipelineEditorView gains an `active` prop; AI-helper registration and the
live-badge poll now gate on isActiveSession && active.
Register the pipeline tools on the session's own chat, not the singleton:
PreviewTabHost mounts the view outside the SessionWrapper subtree that
provides the scoped aiChatManager context, so getAiChatManager() fell back to
the app-wide singleton — build_pipeline_node / edit_pipeline_node never
reached the session chat and the model fell back to write_script (whose draft
never appears on the canvas). Use runtime.manager directly instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): scope list-page preview refresh to the page each tool changes
The scoped-refresh pass reloaded every open list-page preview tab on any
workspace mutation (reloadPages: boolean), so creating a schedule also
refreshed the Resources / Variables tabs.
Replace the blanket flag with the specific page paths each tool can change:
write_schedule → /schedules, write_resource → /resources, write_variable →
/variables, create_folder → /folders, write_trigger → the trigger kind's page;
delete/deploy/discard/rebase map their `type` to its page (none for
script/flow/app). Item-editor writes now reload no pages — their live editor
self-syncs. reloadTabs refreshes a list-page tab only when its own path is in
the touched set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(sessions): drop the inert item-reload path; extract a tested previewReload module
Post multi-target, every editable item is a live editor whose reload() no-ops,
and the one iframe item kind (legacy drag-drop apps) is never emitted as a
scope — so the whole `scopes` half of the preview-reload machinery could never
fire. Remove it (PreviewKind, PreviewScope, scopeKey, itemTypeToPreviewKind,
pendingScopes, and the item-route branch of reloadTabs); the `pages` path
already covers every real reload.
Lift the surviving pure logic out of the 900-line route component into
previewReload.ts — toolReloadEffect(name,args) -> {pages} and a new
tabsToReload(tabs,pages) mirroring selectPreviewTabsToClose — and cover it with
previewReload.test.ts (per-tool page mapping, item kinds reload nothing, the
unknown/local-tool silent-stale guard, loc-over-url matching).
Also clear session.target leftovers: delete the unread EDITOR_TARGET_KINDS
export and rewrite five comments that still described the removed single-target
pane / target-record write.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(sessions): state the preview-reload self-sync invariant once
Consolidate the "live editors self-sync, only list pages reload" rationale
to previewReload.ts and drop the drafting-history phrasings the review
flagged: the update_user_instructions incident and the "(not the runtime)"
contrast in sessionDraftCodecs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): follow the editor cell when a live tab retargets
Address PR review findings on the multi-target preview.
P1 (Codex) — draft sync stayed bound to the old cell after an in-place tab
retarget. useUserDraftSync captured `codec` once, but navigate() re-points a
live editor tab (script/flow/raw_app) to another item without remounting, so
path/workspace/ready followed the new item while the codec still read/wrote the
previous cell's store — cross-writing drafts. Make `codec` a reactive getter
like the hook's other inputs; SessionEditorTarget rebuilds it per path.
P2 (Claude) — navigate() now enforces the single-pipeline-tab invariant that
open() does: retargeting to a /pipeline/<folder> route focuses and re-points the
existing pipeline tab instead of turning the active tab into a second editor
racing the shared pipelineEditorState.
P2 (Claude) — the deploy-in-session handler peeked an editor slot via the
create-on-miss cell accessors, allocating an empty cell for items with no open
tab. Add a non-creating runtime.loadedEditorPath(kind, path) and use it.
P2 (Claude) — correct a SessionPicker comment left stale by the session.target
removal (the preview no longer seeds from a target).
Tests: two navigate() pipeline-invariant cases. npm run check 0 errors; 167
session unit tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: constrain script/flow/raw-app editors to container height in session preview
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: clip session preview picker dropdown to popover so it stops overflowing the page
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: universal styled markdown code blocks with copy button and subtle scrollbar
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sessions): use the shared DraggableTabs for the preview tab strip
The session preview tabs were bespoke markup; converge them onto the same
DraggableTabs component the raw-app editor uses, gaining drag-reorder and
keyboard nav. The active tab keeps its breadcrumb/router picker via a new
tabAccessory snippet, and tabs persist their new order.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): keep the new-tab + button right after the last tab
Add an afterTabs snippet to DraggableTabs that renders inside the scroll row
after the tabs (unlike trailing, which stays pinned outside it), and move the
session preview "+" there so it sits next to the last tab.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(tabs): use the subtle ScrollableX scrollbar for Tabs/TabsV2 headers
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(sessions): use bg-surface for the preview tab strip
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(tabs): add subtle shadow-sm to the selected DraggableTabs tab
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(tabs): drop selected-tab shadow; session strip bg-surface-secondary/50
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(sessions): drop persistent bg on preview bar buttons, hover-only
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(scrollbar): share a .scrollbar-subtle utility across tabs and chat
Extract ScrollableX's hover-revealed scrollbar styling into a global
.scrollbar-subtle utility (both axes, size via --wm-scrollbar-size), have
ScrollableX consume it, and apply it to the AI chat message list so the chat
scrollbar matches the tabs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: address review — scope HighlightCode copy button, plaintext unknown fences, Tailwind ScrollableX
- HighlightCode: keep the subtle CopyButton + surface chip behind buttonsOnHover
so the ~20 non-markdown callers keep the original light copy Button.
- MarkdownCodeBlock: unlabeled/unknown fences render as plaintext instead of
being mis-colored as TypeScript; added common language aliases (ts/js/py/...)
so real languages still highlight.
- ScrollableX: replace the custom <style> block with Tailwind overflow classes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style: make chat and session-sidebar typing dots slightly smaller
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: address auto-review — powershell fence to plaintext + reorder tests
- MarkdownCodeBlock: drop 'powershell' from the sql group so it renders
plaintext instead of SQL-colored (no powershell highlighter in the map).
- sessionPreviewTabs.test.ts: cover reorder (reorders+persists, ignores
unknown ids / keeps omitted at end, no-op when unchanged).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: keep scrollbar-hidden on Tabs row as TroubleshootFlowTutorial selector hook
codex-review: removing scrollbar-hidden broke the tutorial's '.border-b.flex
.flex-row.whitespace-nowrap.scrollbar-hidden.mx-auto' selector. The class is
inert on the non-scrolling row (ScrollableX owns the scroll) but is kept as the
tutorial's stable hook.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: preserve raw <pre> content in MarkdownCodeBlock (codex-review)
As the universal pre renderer, MarkdownCodeBlock also handles sanitized raw
HTML <pre>text</pre> from rehypeRaw, where the text is a direct child of <pre>
(no <code>). Fall back to that text child so raw pre content isn't dropped to
an empty block. Kitchen-sink sample gains a raw <pre> case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): background jobs tray with detach, approval and preview
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): route exec_datatable_sql through the jobs tray
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): jobs tray — orange queued badge, 5-recent pagination, drop remove button
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): silence dev-only false-positive binding warnings
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): silence dev-only false-positive binding warning in FlowEditorView
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): auto-expand jobs tray on approval, close modal on resume
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): let the AI set a per-call inline wait before jobs detach
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): auto-resume the chat when a background job finishes while idle
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): merge jobs tray and edits bar into a segmented session bar
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): address review — canceled-job handling, cross-chat poll guard, tests
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): gray chip dot for canceled-only jobs instead of green
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): keep jobs segment right-aligned when there are no edits
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): address /review — drain snapshot, live region, a11y, leading-ellipsis, remove dev harness
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): announce all same-tick job completions; drop redundant aria-live
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): guard poller re-entrancy; datatable error fallback (auto-review P2/nit)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): honor tool formatter on detached job completion; coalesce poller
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): persist tool result formatter so rehydrated detached jobs keep contract
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The runtime image bundles several Go CLIs whose pinned versions were built
with an outdated Go toolchain (go1.21.7 for kubectl/helm), which image
scanners flag for fixable Go stdlib CVEs. Bump each to the latest release
built on the current patched Go 1.26.4:
- kubectl 1.28.7 (EOL) -> 1.36.2 (latest stable)
- helm 3.14.3 -> 3.21.2 (latest v3; staying on v3 to avoid the Helm 4
breaking changes for a bundled CLI users depend on)
- crane v0.20.6 -> v0.21.7
crane is also updated in DockerfileSlim/DockerfileSlimEe (the slim images
don't bundle kubectl/helm). The docker client comes from the floating
docker:29-dind tag, which already rebuilds to a current Go toolchain.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ai-evals CI job's `npm ci` (frontend) failed with:
npm error `npm ci` can only install packages when your package.json
and package-lock.json are in sync.
Missing: picomatch@4.0.5 from lock file
`picomatch` is a floating transitive: svelte-check pulls it as an
`optional peer` at `^4.0.4`, and vite/vitest/tinyglobby at `^4.0.x`. The
lock pinned 4.0.3/4.0.4, but 4.0.5 was published upstream. On a cold-cache
CI runner npm re-resolves those ranges against the registry and picks the
latest (4.0.5), which isn't in the lock — so `npm ci`'s sync check fails.
It passes locally only because a warm npm cache still serves 4.0.4.
Fix: `npm update picomatch --package-lock-only` (npm 10.9.8, matching CI's
node 22) to refresh every picomatch node to 4.0.5 (and the 2.x line to
2.3.2). Lockfile-only, all semver-patch; no package.json change. Verified
`npm ci --dry-run` is back in sync with a cold cache.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipelines): live materialize/dataset edits reflect on the graph; no phantom draft after deploy
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): Save all deploys the open pane's live buffer, not the stale draft snapshot
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): pin deployedFromPane to the shipped content so mid-deploy keystrokes still promote to a draft
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): draft rename ping-pong loop, stale rename deploys, inactive-draft input lineage
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): dedupe inferred-lineage overlay against accumulated edges; first draft teardown still captures reads
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): record an authoritative empty read capture on uncaptured draft entries
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): teardown skip compares lineage too, so access-only overrides still persist
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* style(pipelines): compress persist-back guard comments to the invariant
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(ai): flow writer builds approval steps as scripts with getResumeUrls, not identity
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ai-evals): accept rawscript or script for approval step type
The flow-writer prompt allows an approval step to be `type: rawscript`
or `type: script`, but the topLevelStepTypes check pinned an exact
`rawscript` match, so a valid `type: script` approval would fail
deterministically. Let the check accept a list of allowed types.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(sessions): don't mount preview tabs when side panel is collapsed
* fix(sessions): cap metadata max_tokens so Anthropic auto-rename works
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): drop redundant -fork suffix from auto-generated fork names
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): default 'also delete forked workspace' to false
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(copilot): apply metadata max_tokens cap on the OpenAI Responses path
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The hover-revealed copy button in the workspace picker sat flush against
the menu's right edge on fork-less rows, because only forked rows render
an expand chevron that insets the copy button. Reserve the chevron's slot
on fork-less rows so copy buttons align across rows and keep right padding.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(copilot): open runs/schedules pages in session preview tabs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(copilot): drop buggy in-place nav, always chip outside a session
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(copilot): open_page covers variables/resources/assets/audit-logs/settings, perm-gated
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(copilot): open_page adds folders, groups and all trigger kinds (EE-gated)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(copilot): close_page tool to close session preview tabs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(copilot): fail-closed on unavailable trigger_kind in open_page handler
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(copilot): gate open_page on operator_settings, keep open_preview mention preview-only
Gate the open_page page set on the workspace operator_settings for operators
(mirrors OperatorMenu) instead of hardcoding runs/assets, with an empty-enum
guard. Also move the open_preview cross-reference out of the always-on prompt
line into the preview-gated block so it isn't advertised when preview tools
are off.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(copilot): gate open_page on the session's operating workspace
A session chat targets its own (possibly forked) workspace while $workspaceStore
stays on the navigation workspace, so operator_settings must be read for the
operating workspace, not the global store. Thread it through GlobalToolHelpers
so both setSchema (advertised enum) and the handler guard gate on the same
workspace; the global side-panel chat still follows the live store.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): scope fork session Edits to session-edited items only
A session chat with an undefined modified-items mask fell back to showing every draft in its (possibly forked) workspace, so the Edits bar/diff drawer listed all fork drafts instead of just what the session edited. Always track session chats: seed an empty mask for legacy chats in loadPastChat and guard the not-yet-persisted-chat case in initRuntime.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: clarify session chats always persist their modified-items mask
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: add parent_workspace claim to OIDC job tokens for fork workspaces
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor: rename claim to fork_parent_workspace for clarity
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: update ee-repo-ref to e2df172596e00877068d4b0a98afaef62fe429d1
This commit updates the EE repository reference after PR #651 was merged in windmill-ee-private.
Previous ee-repo-ref: f73001ac6c038694cfc2604233a59be1c0daa40b
New ee-repo-ref: e2df172596e00877068d4b0a98afaef62fe429d1
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat(sessions): prototype session-mode layout wrapper (design exploration)
Do not merge — design exploration of an optional full-page 'session mode' layout for AI sessions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sessions): add full-screen toggle for the session panel
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sessions): workspace-tree rail with browse mode and collapsible sidebar
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sessions): restore sessions page with iframe preview of current view
Roll back the session-mode layout wrapper: sessions is a dedicated /sessions
page again rather than a layout toggled over the live app. Opening a session
from a Windmill page captures that page as the session's preview target; the
page shows the chat beside a preview panel that iframes the target, with a
breadcrumb and full-screen toggle.
- Remove SessionShell wrapper and the sticky sessionLayout flag; +layout.svelte
always renders the normal global sidebar. Sidebar components introduced
alongside the wrapper are kept for the upcoming sidebar rework.
- sessionMode.svelte.ts: per-session preview-URL map (captureSessionView /
sessionPreviewUrl) + withMenuHidden to drop the previewed page's own sidebar
via the nomenubar flag.
- Drop the #content sidebar gutter (pl-12/pl-40) when the menu is hidden, so
the nomenubar preview fills the panel edge-to-edge.
- SessionPicker: activate() navigates to /sessions; createAndOpen() seeds the
new session's preview from the current page.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sessions): add exit (X) button to chat header
Add a close button at the top-right of the session chat header that leaves the
sessions page and navigates to the session's target (the previewed page), so
exiting lands on exactly what was being previewed, full-screen with the sidebar.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sidebar): promote workspace picker and widen the sidebar
Replace the Windmill logo header with the workspace picker so the active
workspace is the sidebar's anchor: show the workspace name (not the id) in a
stronger weight, with a down-chevron and a bottom-aligned dropdown. Add the
same dropdown chevron to every other sidebar menu trigger (Favorites, User,
Settings, secondary/Help groups) via an opt-in MenuButton option, and widen
the expanded sidebar from w-40 to w-48 (content offset kept in sync).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sessions): collapsible preview panel + sidebar session entry polish
Add a collapse control to the sessions preview panel (top-left, matching the
legacy editor's PanelRightClose), animated with an x-axis slide. The panes
carry no explicit size so Splitpanes auto-distributes — the chat fills the
width when the preview collapses and splits evenly when both are shown. When
collapsed, a floating "Open side panel" Button (top-right) brings it back.
Also gather the AI sessions section into the Favorites/Search container via a
new embedded mode on SessionPicker, replace the small "+" with a full sidebar
"New AI session" entry, and drop the chat header's exit (X) button.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sessions): split family/fork picking with a global breadcrumb
Separate workspace-family selection from fork selection. The sidebar
workspace picker now lists families (roots) only and shows the active
family name even inside a fork. A persistent `family · fork` breadcrumb
lives in the global logged layout (WorkspaceBreadcrumb, rendered via a
new AiChatLayout topBar snippet): the fork segment opens the fork picker
popover, staging a pending fork on a draft session (the old in-chat
SessionWorkspaceBar semantics) or switching workspace directly elsewhere.
WorkspaceFamilyPicker gains onRequestCreateFork to route create-fork to
the global fork modal in non-session contexts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* revert(sessions): drop the global fork breadcrumb top bar
Remove WorkspaceBreadcrumb and its AiChatLayout topBar wiring; restore
the in-chat SessionWorkspaceBar for draft fork-picking and the original
WorkspaceFamilyPicker. The sidebar workspace picker stays family-only
(roots, no forks listed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sessions): family/fork-scoped sidebar with scope header
Restructure the sidebar into a family-scoped region (workspace family
header → New AI session → session list) and a workspace-scoped region
(a Fork scope header → Favorites + Search → workspace items), split by a
full-width divider. The new WorkspaceScopeHeader is a full-width
root/fork picker: accent-styled on a fork (text + faded border), with a
bottom "<workspace> settings" link; picking a different fork from a
session navigates home. The family header keeps the root's color when
inside a fork, and drops "Fork current workspace" / "Workspace settings"
(now surfaced via the scope header and the bottom Settings dropdown).
The session preview header shows "family · fork <page path>".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): drop colon from "Workspace root" scope label
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sessions): turn the preview breadcrumb into a page router
Every breadcrumb segment now opens a drill picker that lists workspace pages
(Home, Runs, Workspace settings, …) alongside scripts/flows/apps. Picking
either steers the preview iframe without leaving the sessions page. The
non-item case resolves to the page's real name (e.g. "Workspace settings").
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sessions): tabbed preview with mounted tabs + Home quick-access
The preview is now a tiny tabbed browser: the first tab is pinned to the
session's view, "+" opens the router picker to add more, and every tab stays
mounted (stacked + visibility-toggled) so switching preserves each page's
state. Per tab, the commanded `url` is decoupled from the observed `loc` so
in-iframe navigation never reloads the frame. Home is also pulled up as the
first quick-access item in the router picker.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sessions): persist preview tabs with the session in IndexedDB
Save the open preview tabs (+ active tab) onto the session record so reopening
a session restores its tabs. Write-behind is debounced since a tab's observed
location churns as the user browses; transient (unsent) sessions skip it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(sessions): lazy-mount preview tab iframes
Only boot a tab's iframe the first time it's activated, then keep it mounted.
Restoring a session with N saved tabs now boots just the active tab instead of
N full Windmill apps at once.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sessions): fold the breadcrumb picker into the preview tabs
Drop the separate family·fork/path breadcrumb bar. The active tab now doubles
as its own router picker (click it to re-point the tab); inactive tabs switch
on click. Removes the now-unused PreviewRouterSegment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(raw-apps): auto-compact the editor when it opens narrow
On the first measured layout, if the editor container is under 800px, drop to
the merged single-pane view and retract the file sidebar (e.g. when shown in
the narrow session preview pane). Applied once on open; the sidebar is set
without persisting so it never overrides the user's saved preference.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sessions): auto-refresh preview tabs after mutating chat tools
Add a tool-completion hook in the shared chat dispatcher; the sessions page
subscribes and debounced-reloads every mounted preview tab when a write/deploy/
delete tool finishes (matched by verb prefix, so read/test/navigate tools skip).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): keep nav bar hidden across in-preview navigation
The sessions preview iframes load pages with `nomenubar=true`, but the
layout recomputed `menuHidden` from the current URL on every navigation,
so a client-side nav inside the preview (an in-page link or redirect)
dropped the flag and the global nav popped back in. Make the hidden state
sticky for the document's lifetime when running inside an iframe; the top
window is unaffected so the oauth-callback toggle still works.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): persist hidden nav across full reloads in preview iframe
The in-memory sticky flag was lost on a full document load inside the
preview (a navigation that drops the `nomenubar` query param), so the
global nav — including the mobile burger — reappeared. Store the sticky
state in sessionStorage so it survives full reloads within the iframe's
browsing context. The top window is unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): refuse to mount sessions UI inside a preview iframe
A preview tab navigating back to /sessions would mount another sessions
page with its own preview iframes, nesting endlessly. When the page
detects it is running inside an iframe, render a stub that breaks out to
the top-level window instead of mounting the full UI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sessions): Workspace ⇄ AI Sessions mode switch + workspace-decoupled session chat
Add a route-derived mode switch that flips the sidebar rail between the classic
workspace navigation and a dedicated AI-sessions sidebar, cleanly separating
sessions from the workspace nav.
- SessionModeSwitch (Workspace | AI Sessions) in the rail; session mode is
exactly "on /sessions", so the switch just navigates in/out (sessionSwitch).
- Session chats target their own (possibly forked) workspace via
AIChatManager.operatingWorkspace/workspaceResolver without mutating the global
workspaceStore; "Acting on" header strip shown once a session has started.
- Flow editor AI button becomes "Open in AI session": saves the draft, then
opens a new session targeting the current flow.
- New sessions: no default preview (empty state instead of iframing home, panel
collapsed); preview-panel collapse persisted per-session on the record.
- Persist nav-rail collapse (manual toggle only) and drop the editor-route
auto-collapse that fought it.
- Smaller fork picker; add a `preview` proxy so `vite preview` reaches the
backend for production-build demos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): replay assistant turns verbatim so thinking blocks validate
The global AI chat reconstructs each assistant turn from an OpenAI-shaped
message, keeping only the thinking/redacted_thinking blocks and re-injecting
them at the front of the content array. When a turn interleaves thinking with
the native web_search tool and ends in a tool call, this reorders the thinking
blocks and drops the server_tool_use / web_search_tool_result blocks. Anthropic
validates each thinking block's signature against the blocks that precede it in
the latest assistant message, so the replayed turn is rejected:
400 invalid_request_error
"messages.N.content.M: `thinking` or `redacted_thinking` blocks in the latest
assistant message cannot be modified. These blocks must remain as they were
in the original response."
Preserve the full `finalMessage.content` verbatim (`_anthropicContent`) and
re-emit it unchanged, instead of extracting and reordering thinking blocks. Skip
the standalone text message that the streamer emits for the same turn (its text
is already inside `_anthropicContent`). The previous thinking-only path is kept
as a fallback for sessions persisted before this change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(sessions): give empty-state preview picker its own open state
* feat(sessions): render preview editors as components, not iframes
Introduce a PreviewTabHost seam that routes each preview tab to either an
in-process editor (the session's script/flow/raw_app target, reusing the
existing *EditorView wrappers + shared runtime) or an iframe fallback for
pages and other items, behind a uniform reload(). resolvePreviewTab classifies
a tab from its URL + the session target.
Also intercept in-iframe navigation to an editor route (logged layout
beforeNavigate): post the target up to the sessions page, which promotes the
active tab to the live editor component, so an editor is never booted inside
an iframe.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): drive open_preview tool through the multi-tab model
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: plan SessionPreviewTabs deep module for sessions preview tabs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(sessions): own preview tabs in a SessionPreviewTabs deep module
Collapse the three drifting preview-tab copies (page-local state, session
record, legacy previewUrls localStorage) into one live owner held on
SessionRuntime.previewTabs. Both the sessions page (renderer) and the
open_preview/get_preview_status tools cross it, so both sync effects and the
localStorage seed disappear; url/target writes become atomic.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): gate the Workspace/Sessions switch behind the global-AI dev flag
The SessionModeSwitch is the only entry point into the AI-sessions
experience, so gate it on wm_dev_global_ai like the global chat and the
sessions page — otherwise the unfinished mode ships to prod.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): pin the settings footer and normalize row text in the fork dropdown
Split the family picker menu into a scrollable body + a pinned settings
footer so the workspace-settings link stays visible while the fork list
scrolls. Give every row a uniform text-primary font-normal style (rows
were inheriting a bold 600 weight; the settings link was text-secondary).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sidebar): fold theme switch into the settings dropdown and keep it in session mode
Move the Switch-theme toggle into the sidebar Settings dropdown and reorder
its entries (bottom-to-top: Instance, Workspace, User). The dropdown now
renders in both navigation and session modes; session mode hides only the
workspace-settings entry (the rail's global workspace doesn't map to a
session's forked workspace).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(fork): validate fork name/id length before creation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): title open-in-workspace button "Open in workspace"
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): keep AI chat working when the sessions dev flag is off
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): match burger drawer width and keep it open on mode toggle
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sessions): surface the dev-workspace badge across session workspace pickers
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): dedup navigate, sanitize hydration, cap mounted tabs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(fork): support forks of forks via a base-workspace picker
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sidebar): family expansion, pinned menu actions, animated popovers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(sessions): persist unsent drafts, gate preview, loading state
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sidebar): group fork picker on top and unfold the session list
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sessions): capture splitter pointer so off-window release ends drag
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(sessions): retire the pinned preview tab (dot and no-close)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(sessions): shared open-in-AI-session button across editors
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sessions): dedup page tabs, flush on hide, review cleanups
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(sessions): give unsent drafts a side panel, reset tabs on retarget
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sessions): keep the session fork icon neutral except when detached
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sessions): scope session-mode restore and transient reuse to family
* fix(sessions): preserve session mode across workspace switches
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sessions): reconcile open session with family on workspace switch
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sessions): lazy-load runtime in session switch to keep it node-testable
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(sidebar): add bottom brand mark and standalone workers/logs links
* feat(sidebar): add name+id copy tooltip to workspace picker
* style(sidebar): add spacing between settings and brand mark
* feat(forks): id-based fork creation, fork color theming, picker polish
* feat(forks): copy-id in session header, inert chip, fork form polish
* fix(sidebar): restore logs, help, user and leave-workspace menus
* feat(sidebar): carry active tick on collapsed family root
* feat(dev): add settings-menu kitchen sink page
* fix(sessions): fail closed for unbound persisted sessions in family scope
* fix(sidebar): keep workspace URL param in sync across switches
* feat(sessions): remove home page from preview tab navigation
* refactor(sidebar): dedupe shared helpers and address review findings
* feat(sessions): keep preview hosts alive across session switches
* feat: workspace settings links in session rail, acting badge and family picker
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: refresh session changes bar after out-of-window deploy
The session "Edits" bar re-fetched its draft list and existence checks
only on AI turn-end, tab visibilitychange, and drawer open. Deploying an
item from a full-page editor in a second browser window left the bar
stale: that tab never goes hidden, so visibilitychange never fires, and
the badge kept reading "1 draft" while opening the drawer showed no
pending change.
Add a window `focus` listener alongside visibilitychange so returning to
the session window re-syncs the bar, and refresh the dock when a badge is
clicked so the drawer always opens on fresh state.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(sessions): show name/id copy tooltip on acting badge, drop inline copy
* fix: keep editor header cloud indicator visible at narrow widths
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(sessions): scope preview reload to the mutated item
Reloading every mounted preview tab on any mutating chat tool blank-
rebooted unrelated raw-app previews: a raw app that isn't the session's
live-editor target renders as an /apps_raw/edit iframe, and reloadAllTabs
hard-reloaded it (frame.location.reload) on every write/deploy elsewhere.
Pass the tool args through the completion listener and scope the reload:
an item-route iframe reloads only when its item was actually touched. The
changed item is args.path for workspace-path tools; the raw-app file tools
(write_app_file, …) pass a leading-'/' frontend file path and edit the
active session's target app, so scope to the target; anything else is
unresolved and reloads everything (safe fallback). Changed paths accumulate
across the 500ms debounce. Non-item pages still always reload; live-editor
slots still no-op.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(diff): honor side-by-side/unified toggle and widen draft drawer
Monaco forces inline view below its 900px renderSideBySideInlineBreakpoint, which overrode our SIDE_BY_SIDE_MIN_WIDTH gate and made the toggle a no-op in the ~800px draft drawer. Disable useInlineViewWhenSpaceIsLimited so our width logic wins, and widen the drawer default 1200->1500px.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(diff): vertically center the element-header icon with its path
The path renders as ExternalEditLink's inline-flex <a> in production, which sat ~2px low on the wrapper's line-box baseline. Make the path wrapper flex+items-center so the icon and path align by box.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(diff): reflect the auto-unified downgrade in the drawer view toggle
The side-by-side/unified downgrade lived inside each DiffEditor's width gate, so the drawer toggle still showed side-by-side when the narrow column rendered inline. Measure the diff column, make the drawer authoritative (force inline when narrow), and reflect it in the toggle (unified selected, side-by-side disabled) while preserving the user's preference for when it widens again. Shared SIDE_BY_SIDE_MIN_WIDTH via diffEditorTypes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(sidebar): make the nav rail resizable with rem scaling
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(diff): gate Monaco auto-inline behind a prop to keep narrow diffs unified
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ui): restore instant popover/dropdown default, opt sidebar and sessions in
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sidebar): restore delete-forked-workspace action in the settings menu
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(tooltip): add cursor anchoring option and use it for the name/id tooltip
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(dev): remove settings-menu kitchen sink scaffolding
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(docs): remove session-preview-tabs owner plan doc
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(dev): drop vite preview-server proxy scaffolding
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sidebar): scroll nav as one block with fade hints, pin settings to bottom
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sidebar): guard against concurrent pointer drags leaking resize listeners
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: scope session preview, LLM proxy, and raw-app workspace switch
Address PR review findings: session preview iframes, the AI chat LLM
proxy client, and the raw-app workspace-switch guard all now resolve the
session's effective workspace instead of the global navigation workspace.
- withMenuHidden appends the session workspace as ?workspace= so preview
iframes render fork-scoped pages against the fork, not the nav workspace.
- AIChatManager builds the proxy clients from operatingWorkspace so the
LLM request hits the session workspace's /ai/proxy, not the global
singleton (init'd only on global workspace changes).
- workspaceSwitchUrl adds /apps_raw/edit|get to EDIT_PAGES so switching
workspace from a raw-app editor/viewer goes home like other item pages.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: scope session model, preview picker, and open-in-workspace to session
Second-layer workspace-scoping fixes from PR review: three more paths
resolved the global navigation workspace instead of the session's
effective workspace.
- SessionWrapper loads copilot config (models/providers) for the session's
acting workspace, so getCurrentModel/modelProvider match the workspace
the chat writes to, not the nav workspace.
- PreviewRouterPicker takes a workspaceId prop; the sessions page passes the
session's effective workspace so the breadcrumb/+ picker lists fork items
and its drafts, not the nav workspace's.
- 'Open in workspace' appends ?workspace= via the new withWorkspaceParam so
the full-page link opens the active preview under the session workspace.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): only the active session loads global copilot config
Follow-up to the session-workspace copilot fix: SessionWrapper's
loadCopilot effect ran in every warm/hidden wrapper, and since
copilotInfo/copilotSessionModel are global, a background session in a
different workspace could finish loading after the active one and leave
the active chat on the wrong provider/model. Gate the load on
currentSessionId so only the active session writes the shared config.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): guard copilot load race + scope app handoff to workspace
Two more session-vs-navigation workspace fixes from PR review:
- loadCopilot now applies only the most recent call's result via a
monotonic token, so a stale async load from a just-switched-away session
can't clobber the active session's global model/provider config.
- navigateEditorTo carries the session workspace on the low-code app
handoff (goto /apps/edit) so the app opens in the fork the session acts
on, not the navigation workspace.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): scope live-editor breadcrumb picker to session workspace
The session preview's live script/flow/raw-app editors mount with a
session workspaceId, but their EditorHeader breadcrumb picker
(WorkspaceItemDrillPicker) still loaded items and drafts from the global
navigation workspace. Thread an optional workspaceId prop from each
builder's autosaveWorkspace through EditorHeader -> BreadcrumbSegment ->
WorkspaceItemDrillPicker; it falls back to $workspaceStore, so non-session
editors are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(sessions): fix stale setSessionTabs transient-persistence comment
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): scope live-editor deploy/save/triggers to session workspace
The session preview's live script/flow/raw-app editors load and autosave
against the session's acting workspace, but their internal deploy,
save-draft, trigger-loading, fork-eligibility, worker-tags and
live-editor-draft operations read $workspaceStore directly. Since a session
deliberately leaves $workspaceStore on the navigation workspace, a
fork-scoped session deployed/saved to the wrong workspace (verified: deploy
POSTed to the nav workspace and 400'd).
Introduce an opWorkspace derived (autosaveWorkspace ?? $workspaceStore) in
each builder and route the operation reads through it. autosaveWorkspace is
only set by the session editor views, so opWorkspace equals $workspaceStore
for every non-session editor — no behavior change outside sessions. Verified
in-browser: a fork-session deploy now POSTs to the fork (201 Created) while a
normal editor still targets the navigation workspace.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): staged pending fork chip uses default accent, not parent's color
A staged pending fork's effective workspace resolves to its parent
(setSessionPendingFork sets pending_workspace_id = parent_workspace_id),
so WorkspaceScopeTrigger read the parent workspace's color and painted the
'Acting on' chip in the parent's hue (e.g. yellow) instead of the neutral
fork accent. A real fork shows its own color; a not-yet-created one has
none, so fall back to the default fork accent unless the creation form
passes an explicit color preview.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): scope raw-app deploy/save/version to session workspace
The raw-app create/update/version/diff/save operations live in
RawAppEditorHeader (not RawAppEditor), and still read $workspaceStore — so
a fork-scoped session's raw-app deploy targeted the navigation workspace,
the same class of bug already fixed for scripts and flows. Route those
operation reads through opWorkspace (autosaveWorkspace ?? $workspaceStore);
the inSessionPane-guarded draft-cleanup blocks are intentionally
non-session and keep $workspaceStore. Verified in-browser: a fork-session
raw-app deploy POSTs update_raw to the session fork (200).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): key live-editor load cache on workspace, not just path
The script/flow/raw-app loaders returned early when loadedPath matched the
requested path, ignoring the workspace. Retargeting a session to the same
item path in a different fork kept the old workspace's loaded content while
the editor props switched to the new workspace — so save/deploy/autosave
could write stale old-workspace content into the new fork. Add
loadedWorkspace to the load slot and include it in the early-return guard so
a same-path/different-workspace retarget reloads. Verified in-browser: the
script re-fetches from the new fork on an acting-workspace switch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): drop stale content when a live editor retargets to a new workspace
Follow-up to keying the load cache on workspace: the loaders reloaded on a
workspace retarget but did not clear loadedPath during the fetch, so
SessionEditorTarget's loadedPath-keyed ready/notFound/stale gates still
treated the editor as ready on the old workspace's content — the outbound
draft sync (now wired to the new workspace) could write stale content into
the new fork, and a 404 kept rendering the old editor. Clear loadedPath on a
workspace change too (like a force reload), so the loading/not-found gates
and the draft-sync ready check resolve correctly. Same-workspace path swaps
are unaffected (loadedWorkspace still matches, so the old editor stays
visible during the swap).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): await committed-workspace copilot config before a session send
getCurrentModel() reads the global copilotInfo when the request builds, but
SessionWrapper's loadCopilot for the active session is fire-and-forget — so a
send right after switching to a session in another workspace could pick the
previous workspace's provider/model while the proxy clients and tools target
the new workspace. Track the workspace copilotInfo reflects (copilotWorkspace)
and, in the session beforeSend hook (awaited before the request builds), load
the committed workspace's config when it doesn't already match. Verified
in-browser: sending a session committed to a workspace whose copilot config
wasn't yet loaded fires get_copilot_info for it just before the LLM proxy call.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): navigate to a fresh session on reset; dedupe preview page tabs
Two review findings:
- resetToNewSession (deleting/archiving the open session) and the sidebar
delete of the active last session created/selected a fresh session but left
the URL on the old session_name. The page derives the visible session from
that query, not currentSessionId, so it showed the deleted session's
not-found state (or stayed on the archived one). Navigate to the fresh
session, matching how activate()/enterSessionMode already switch sessions.
- Preview page-tab dedupe: the iframe reports its location with the injected
nomenubar/workspace params, but tabs dedupe the observed loc against the
workspace-less canonical url, so reopening a page spawned a duplicate tab.
Canonicalize the observed loc in observeLocation (dropping both params);
covered by a new sessionPreviewTabs test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): don't persist preview-iframe workspace; scope fork ducklakes to base
Two review findings:
- A sessions-preview iframe runs the logged layout, which persisted its
?workspace= (the session's fork) to localStorage — shared with the
top-level app, so opening a fork preview clobbered the navigation
workspace and reloads restored into the fork. Skip the persist when
embedded; $workspaceStore is still set in-memory for the iframe's own API
calls. Verified: opening a fork /runs preview leaves localStorage.workspace
on the top-level workspace.
- ForkDucklakeSection listed ducklakes from $workspaceStore while a
fork-of-fork is created from the selected base, so it could show the root's
lakes and submit shared_ducklakes the base doesn't have. Add a
sourceWorkspace prop (base ?? $workspaceStore) like ForkDatatableSection,
and pass baseWorkspaceId at the mount.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): keep preview iframe on session fork across reloads and open-in-workspace
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sessions): scope worker-tag pickers to the session's effective workspace
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: add datatable_migrations table
* feat: add route to run datatable migrations
* feat: sync datatable migrations as .up.sql/.down.sql files
* feat: add datatable migrate up/down commands and post-push run prompt
* feat: add datatable migrate new command to scaffold migrations
* feat: add datatable migrations management UI
* feat: prompt to create migration on DDL in datatable SQL editors
* feat: support running a single specific datatable migration
* feat: view migration content, run single migration, fix stacked modal
* feat: per-row revert button with out-of-order warning
* fix: avoid migrations list flicker on refresh after an action
* feat: generate initial datatable migration via pg_dump
* fix: surface datatable migration API error details in toasts
* fix: revert created migration if create-and-run fails to run
* fix: include postgres error detail in migration run/rollback failures
* feat: sync datatable migrations as files via the workspace export
* refactor: move datatable migrations to migrations/datatable/ path
* fix: drop redundant datatable_migration label in sync output
* fix: exclude datatable migration sql files from script metadata generation
* feat: run datatable migrations as user-permissioned labeled jobs
* feat: reject invalid datatable migrations on sync push
* feat: datatable migrate up/down default to all datatables, --datatable to target one
* fix: surface postgres error detail when datatable migrations fail to run
* chore: regenerate CLI docs for datatable migrate commands
* feat: default new datatable migration to a BEGIN/END transaction template
* fix: validate datatable migration name and datatable at the API boundary
* fix: ensure detected DDL ends with semicolon when wrapped in transaction
* fix: re-prompt instead of stripping DDL when new-migration modal is cancelled
* feat: refresh datatable schema after running a migration from the SQL REPL
* feat: record db manager DDL on data tables as migrations
* feat: make datatable migrations opt-in per data table
* fix: make migration view editor read-only so its code can scroll
* fix: don't re-prompt DDL guard when creating a migration without running
* feat: generate down migrations for db manager DDL (postgres)
* fix: correct down migration for db manager alters (no double-wrap, serial)
* feat: explain migrations purpose with a tooltip in the migrations modal
* compare paeg
* feat: add datatable_migration kind to workspace diff pipeline
* chore: point ee-repo-ref at datatable_migration git-sync companion
* fix: harden datatable migration version allocation and initial-migration bookkeeping, add tests
* feat: deploy and run datatable migrations on workspace merge
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Refactor + handle datatable setting delete/rename
* refactor: move datatable migration rename/delete cascade into module
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(windmill-utils-internal): bump to 1.7.1 for datatable migration deploy provider methods
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(db-manager): add Migrations button to top bar, make Refresh icon-only
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* BEGIN/END placeholder in down migration
* feat: autofocus migration name input and flag it red when empty
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(datatable-migrations): allow non-admins to create/run/revert migrations, gate only opt in/out
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* border nits
* refresh db manager schema on migrations
* BEGIN/END scaffold in CLI
* feat(cli): push local datatable migrations before running on migrate up
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: flag invalid migration name with red border, not just empty
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor: drop random slug from auto-generated migration names
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: offer revert-and-delete when deleting an installed migration
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: record fork merge as a migration when target datatable opts in
* nit
* clone migrations on fork
* windmill-utils-internal
* fix(datatable-migrations): serialize run/rollback with a per-db advisory lock
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(db-manager): fail closed when migrations-status check errors on DDL apply
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: fix generate_initial migration ordering comment to match code
* chore(datatable-migrations): remove unused update_datatable_migrations endpoint
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: run DDL migration guard on the script editor Test button
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* split
* ee-repo-ref
* chore(frontend): sync package-lock with package.json (@emnapi deps)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(datatable-migrations): never resolve instance credentials into migration job args
datatable_database_arg eagerly resolved instance data-table credentials
(including the shared instance-wide Postgres password) and passed them as the
migration job's plaintext `database` arg, landing in v2_job.args. Since the
run route has no admin gate, a non-admin could run a migration and read
args.database to recover the password, granting cross-workspace psql access to
all instance data-table DBs.
Pass a `datatable://<name>` reference for both resource-backed and instance
data tables instead; the pg executor already resolves it to real credentials
server-side at run time, so nothing sensitive is ever stored in the job args.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* nit
* fix: handle dollar-quoting and comments when splitting SQL statements
* feat: deploy datatable migrations on merge with explicit opt-in error
* fix(frontend): sync package-lock with npm 11 peer-dep resolution
npm ci failed with 'Missing: @emnapi/core@1.11.2 / @emnapi/runtime@1.11.2 from
lock file'. @napi-rs/wasm-runtime declares @emnapi/core|runtime ^1.7.1 as
peerDependencies while @rolldown/binding-wasm32-wasi pins them to exactly
1.10.0. Newer npm (bundled with node 24 in CI) installs the peer deps at the
highest match (1.11.2) alongside rolldown's nested 1.10.0, so the ideal tree
needs both versions; the committed lock only had 1.10.0.
Regenerate the lock with npm 11.18 so it carries both 1.11.2 (top-level, for
the peer deps) and 1.10.0 (nested, for rolldown's pin). Verified npm ci passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* nit npm publish
* fix: fail closed on migrations-status error in fork schema merge
* nit CI emnapi/core version
* prevent initial_datatable_migration if migrations already exist
* fix(datatable-migrations): validate persisted data table names as path segments
edit_datatable_config only validated rename segments, not the actual
settings.datatables keys, so a data table could be saved directly under a name
like '..' or one containing '/'. Since new tables default to
migrations_enabled = true, generate_initial_datatable_migration would then
insert a migration row and the sync export would build
migrations/datatable/<name>/... paths from that name, producing malformed or
directory-escaping export paths.
Validate every persisted data table name in edit_datatable_config (alongside
the existing rename checks) and add validate_datatable_path_segment to
generate_initial_datatable_migration for defense in depth.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: scope datatable _wm_migrations by data table and cascade renames/deletes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(system_prompts): resolve nested local command groups in CLI docs generator
The CLI docs generator anchored on the first `new Command()` in a file and
never resolved locally-defined command groups passed as
`.command("name", localCmd)`. For datatable this flattened the nested
`migrate` group: it emitted `datatable new/up/down` plus a bare
`datatable migrate`, and mislabeled the datatable command with the migrate
group's description. jobs was broken the same way (its description was pull's,
and pull/push rendered empty).
Anchor block extraction on the `export default`ed command, recurse into
locally-defined `const x = new Command()` groups mounted as subcommands, and
render nested sub-subcommands. Regenerated docs now show
`datatable migrate new/up/down` and `jobs pull/push` with their real
options.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor: drop unreleased _wm_migrations legacy-upgrade handling
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: return datatable migration SQL from getItemValue for the diff drawer
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(frontend): use windmill-utils-internal 1.8.2 for migration diff drawer
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* nit
* nit
* fix: handle datatable migration renames on push and dedupe timestamps
* fix: reject rewriting an already-applied datatable migration on upsert
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): add missing @emnapi/core and @emnapi/runtime lockfile entries
Resolves npm ci EUSAGE failure: the optional cpu:wasm32 @rolldown/binding-wasm32-wasi
declares deps on @emnapi/core@1.11.2 and @emnapi/runtime@1.11.2 that had no resolved
lockfile entries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): datatable migrate up/down default to main datatable, not all
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: fail closed when applied status unreadable on datatable migration rewrite
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: surface full error detail in Database Manager DDL/query errors
* "See migration" button in the toast
* feat: add Enter shortcut to Create-a-migration in the DDL guard
* fix(frontend): warn before running a newly-created datatable migration out of order
The row-level Run action warns when earlier migrations are still pending, but
the create-and-run paths ran a just-created migration with `only` directly,
applying it ahead of older pending migrations without that confirmation.
Reuse the same "Run migration out of order" confirmation across all
create-and-run paths via a shared helper (datatableMigrationUtils):
- NewDataTableMigrationModal "Create and run" (and the DDL guard path)
- DatatableSchemaDiff fork→parent merge
- dbOps schema ops (DB manager create/alter/drop) — the pure factory throws a
MigrationRunCancelled sentinel on decline, which DBTableEditor treats as a
silent cancel
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: keep renamed datatable migrations visible in compare view
* fix: record per-migration deployment on datatable migrations disable
* fix(cli): run deployed datatable migrations after workspace merge
The merge command upserted datatable_migration definitions into the target
workspace and reported the item as successfully deployed, but never ran the
migrations. For forked datatables backed by separate databases, this left the
target schema unchanged until someone manually ran `wmill datatable migrate up`,
while the CLI reported a successful merge.
Collect the datatable migrations deployed (not deleted) into the target and,
after the deploy loop, offer to run them via the existing offerToRunNewMigrations
helper — the same post-deploy run prompt the push/sync path uses (interactive
only; `--yes`/non-TTY skip the mutating run, matching push behavior). Export
parseDatatableMigrationDeployPath so the merge path can parse the deployed items.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(backend): serialize datatable migration edits/deletes with the run lock
A migration run snapshots a migration's code_up from datatable_migrations and
only records its version in the data table's _wm_migrations after the job
succeeds. upsert_datatable_migration checked _wm_migrations before allowing an
edit but took no lock, so a concurrent edit could read "not applied yet",
rewrite code_up/code_down, and then the in-flight run would record the version
for the old SQL — leaving _wm_migrations pointing at SQL that was never applied
(migrate up then skips it; rollback runs a down that doesn't match).
Serialize definition rewrites and deletes with the same per-database advisory
lock the run/rollback paths use:
- Factor the connect+advisory-lock into lock_datatable_migration_runs and the
applied-versions read into read_applied_versions_on_client.
- run_datatable_migrations now snapshots the definitions AFTER taking the lock,
so code_up can't change between snapshot and version-record.
- upsert (when changing an existing def) and delete take the lock across the
applied-check and the write; delete now rejects deleting an already-applied
migration (would orphan its _wm_migrations record), symmetric with upsert.
Both fail closed if the data table database is unreachable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): stack the out-of-order migration confirm above the DB editor preview
Creating a table on a migrations-enabled data table opened the DB table editor's
"Confirm running the following" preview modal, whose confirm triggers applyDdl,
which then asks for out-of-order confirmation. Both are ConfirmationModals with a
hardcoded z-[9999]; the out-of-order one lives in DBManagerContent (mounted before
the editor), so it rendered behind the still-open preview modal.
Add an optional zIndexClass prop to ConfirmationModal (default z-[9999],
backward-compatible) and give the DB-manager out-of-order confirm z-[10000] so it
stacks on top.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 27672e37df5d9dfde94f19963d5ffcdf8dd5448c
This commit updates the EE repository reference after PR #623 was merged in windmill-ee-private.
Previous ee-repo-ref: 6c287041cd7edd4a77a4bc07ad0e156cec32cce4
New ee-repo-ref: 27672e37df5d9dfde94f19963d5ffcdf8dd5448c
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat(raw-apps): render runtime-error overlay + instruct AI to import React
Render the `runtimeError` message the raw-app preview frame now posts as a
prominent overlay, so an uncaught exception that blanks the app is visible
instead of silent. Cleared on the next successful build (via a shared
`feedPreviewIframe` helper so every preview-feed path resets it).
Add an AI app-generation instruction to begin React files with
`import React from 'react'`: raw apps bundle with the classic JSX transform,
so a missing import compiles fine but throws "React is not defined" at runtime.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(raw-apps): add the import-React rule to the shared raw-app prompt
The global AI chat and the raw-app CLI skill draw their raw-app authoring
reference from system_prompts/base/raw-app.md — a separate surface from the
app chat's inline prompt (core.ts). Add the same "always begin JSX files with
`import React`" rule there (esbuild's classic transform needs React in scope,
or JSX throws "React is not defined" at runtime) and regenerate the derived
prompt files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(raw-apps): bump ui_builder tarball to f8cecf9 (runtime-error overlay)
Pins the ui_builder artifact to windmill-code-ui-builder#15, which pushes
uncaught runtime errors from the preview iframe to the parent so the raw-app
editor can render them in the error overlay.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
migrate() and fix_flow_versioning_migration re-acquired a second connection from the pool while already holding one (the migrator's checked-out, advisory-locked connection). That deadlocks any backend limited to one connection at a time — connection-constrained managed Postgres, PgBouncer transaction pooling, or an embedded single-connection dev database. Route those housekeeping queries onto the already-held connection via a new CustomMigrator::connection() accessor. Fewer connections during migration and, for fix_flow_versioning, the existence check and write now run on the same advisory-locked connection. Default multi-connection behavior is unchanged.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): simplify custom skills workspace settings UI
Collapse the "Custom skills" AI settings section into a single block. When
no skills exist, show two side-by-side zones: a drag-and-drop folder dropzone
(reusing FileInput) and a paste textarea whose add button appears only once
content is entered. When skills exist, an "+ Add skills" dropdown offers
"Import a folder of skills" (native picker) and "Paste a skill" (modal), above
the skills list. Folder ingestion is shared by both the picker and the dropzone
via processFolderFiles.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): custom skills detail modal + shared zod validation
Rework the Custom skills settings: header Add-skills dropdown, per-row
ellipsis menu (edit/delete), a Show more detail modal with a view/edit
toggle (rendered markdown in read mode), accent Save gated by dirty
detection and inline validation, and a folder-import conflict modal with
per-skill overwrite toggles. Extract skill parsing/validation into a
shared Zod-backed aiSkills module used by both the modal and the importer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): use Button for Show more; surface empty-body validation
Address review: swap the raw <button> Show-more affordance for the
design-system Button (per frontend component standards), and render the
Save/inline-error block whenever editing an existing skill so clearing
the body surfaces "body is required" instead of hiding both.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(frontend): unit-test aiSkills; use themed border token
Add aiSkills.test.ts covering parseSkillMd (BOM, CRLF, malformed YAML),
validateSkill (code-point vs byte limits, name pattern), parseAndValidateSkill
(nameOverride precedence) and buildSkillMd round-trip. Replace the hardcoded
gray borders with the themed border-border-light token.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): cap custom skills list height and scroll
Constrain the skills list to max-h-96 with overflow-y-auto so a large
number of skills scrolls within the section instead of pushing the page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(frontend): manage-mode batch delete for custom skills
Add a "Manage skills" button (shown only with more than one skill) that
enters a multi-select mode: a checkbox per row plus a sticky select-all
(tri-state) header, and a batch Delete gated on the selection with a
confirmation. Manage mode auto-exits when the list drops to one skill.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(frontend): destructive delete, subtle manage button, Esc exits manage mode
Batch Delete uses the destructive accent variant, Manage skills uses the
subtle variant, and Escape leaves manage mode (mirroring Done) unless a
modal or menu is open.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(frontend): add neutral 'info' type to ConfirmationModal; use for skill import
ConfirmationModal only had 'danger' and 'reload' semantics, so a
constructive confirmation like importing skills defaulted to danger
(red warning + destructive button). Add a neutral 'info' type (blue Info
icon, non-destructive accent confirm) and use it for the Import skills modal.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: add cosmetic dev/staging label for dev workspaces
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: prefill dev fork name and use a link to switch its label
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style: reword the dev/staging label link copy
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style: preview the dev/staging label as a badge in the switch link
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: show the dev/staging badge in the session diff drawer header
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: mute toggles in critical alerts modal no longer close popover or fail to save
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: mark popover content root as dropdown-portal so padding clicks don't close modal
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: derive no-channels warning from mute state so it survives modal reopen
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(assets): handle card/header overflow on small screens
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(assets): keep filter row label on one line with min spacing from refresh
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(assets): wrap card header actions below title instead of collapsing docs to icon
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(assets): widen card basis to 340px so cards wrap sooner and header stays one row
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(pipelines): auto-derive cascade trigger edges from ducklake/s3 reads
Within a `// pipeline`, a read of a ducklake table or s3 object now
auto-wires its cascade trigger edge straight from the FROM clause, so
`// on <asset>` is only needed for edges inference can't see (dynamic SQL)
or to carry per-edge opts. Two opt-outs: `// mute <asset>` suppresses a
single derived edge (a lookup / SCD input read every run but not cascaded
on), and `// mute all` opts the script out of derivation entirely (back to
explicit-`// on`-only). Explicit `// on` still wins the dedup.
Scoped to ducklake + s3 reads; resource/datatable/volume stay explicit.
Read-write (RW) and write inputs are excluded so a self-referential
merge can't loop-trigger itself; ambiguous (None) access is skipped.
- parser: `mute` / `mute_all` in PipelineAnnotations (Rust + TS mirror)
- deploy: derive_pipeline_asset_trigger_refs → script_trigger rows
- frontend: resolveGraph mirrors derivation for the live edit-mode canvas
- tests: shared parity corpus + derive-helper units + resolveGraph overlays
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(pipelines): mark auto-derived cascade edges with a persisted derived flag + "auto" badge
Persist script_trigger.derived (deploy: true for ducklake/s3-read derivation,
false for explicit // on) and return it from the asset-graph endpoint so the
canvas renders a Sparkles "auto" badge on auto-wired edges — the inference is
now visible on both the deployed graph and the live edit canvas, not just
implied. Dispatch (fetch_subscribers) ignores the flag, so a derived edge fires
identically to an explicit // on. Also copy derived in the workspace-clone
trigger copy, and backfill muteAssets/muteAll into two empty PipelineAnnotations
literals the base commit left stale (check:fast).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipelines): derive cascade edge from effective (alt-fallback) asset access
derive_pipeline_asset_trigger_refs gated on the raw parser access_type, but the
persisted asset.usage_access_type and the frontend canvas both use
access_type.or(alt_access_type). An ambiguous parse with a manual read override
was persisted/drawn as a read yet derived no edge, so the auto edge silently
vanished on deploy. Gate on the effective access type for parity.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(pipelines): badge muted reads instead of auto-derived edges
Auto-derivation is the default now, so badging every derived cascade edge is
noise. Drop the "auto" badge and the persisted `script_trigger.derived` flag
(migration + insert param + graph field + clone copy) that only powered it, and
instead badge the exception: a ducklake/s3 asset a script reads but does NOT
cascade — `// mute <asset>` / `// mute all`. `computeMutedReadKeys` marks a
read-only ('r') supported read with no cascade trigger and no self-write; the
canvas renders a bell-off "muted" badge on that read edge.
Also fixes two review parity nits:
- TS `// on` parser now strips trailing `key=value` opts (e.g. `debounce=60s`)
like the Rust `split_trailing_kv_opts`, so the ref dedups against inference.
- A `// materialize` producer reading its own target is upgraded to `rw`
(deploy) / excluded via the materialize write refs (canvas), so it neither
self-cascades nor shows as a muted read.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipelines): drop redundant // on for auto-derived reads; gate muted badge to pipeline scripts
- Templates no longer scaffold `// on <asset>` for a ducklake/s3 input the body
reads — the read auto-wires the cascade now that derivation is the default.
Kept for datatable/resource (not auto-derived) and native triggers. The
discoverability hint now mentions `// mute` (the newly relevant annotation).
- computeMutedReadKeys only badges reads by `// pipeline` scripts. A plain
script or flow reading a ducklake/s3 asset never had an auto trigger to
suppress, so it must render as ordinary lineage, not "muted" (Codex review).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipelines): only drop template // on when the body actually reads the input
The redundant-`// on` removal assumed the generated body reads the ducklake/s3
input, but postgres/bash/generic bodies (and `data_upload`, which reads the
picker file) ignore `input` — dropping `// on` there left the asset-created
script with no cascade at all. Gate the drop on READS_INPUT_LANGS
(bun/deno/python/duckdb) so non-reading templates keep the explicit trigger.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(auth): add runtime NO_AUTH mode for authentication bypass
Adds a runtime `NO_AUTH` env flag that makes every request resolve as the
`admin@windmill.dev` superadmin with no login required, so self-hosted
deployments can front Windmill with their own authenticating gateway
without building a dedicated `oss` (compile-time `no_auth`) binary.
- `NO_AUTH` is honored in any build but is force-disabled when
`CLOUD_HOSTED` is set, so the managed cloud always enforces real auth.
- The existing compile-time `no_auth` feature keeps its always-on behavior
(`cfg!(feature = "no_auth") || *NO_AUTH`), so `oss` builds are unchanged.
- `Tokened` now yields a synthetic token in no-auth mode so handlers that
require it (e.g. global_whoami, called by the frontend on load) resolve.
- A loud startup banner warns when the mode is on; `HIDE_NO_AUTH_BANNER`
silences it once the operator has deliberately deployed behind a gateway.
Fixes WIN-2131
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(auth): dismissable NO_AUTH warning banner via global setting
Replaces the HIDE_NO_AUTH_BANNER env flag with a UI warning banner that
can be permanently dismissed for all users from within the running
instance (not exposed in instance settings).
- New `no_auth_banner_dismissed` global setting, only ever written by
dismissing the banner itself.
- `GET /api/settings/no_auth_banner` returns whether to show the banner
(true only when NO_AUTH is active and it hasn't been dismissed).
- NoAuthBanner.svelte renders a top-of-app warning in NO_AUTH mode; its
dismiss button opens a confirmation modal, then writes the global
setting via the existing setGlobal endpoint so it stays hidden for
everyone.
- The server still logs the startup NO_AUTH warning unconditionally.
Fixes WIN-2131
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(auth): resolve NO_AUTH in AuthCache so all_runnables works
Codex/Pi review flagged that `/api/users/all_runnables` still failed in
NO_AUTH mode: `get_all_runnables` extracts `Tokened` and re-validates the
request token per workspace via `AuthCache::get_authed`, which rejected the
fabricated `"no_auth"` token (no matching DB row) with a 400.
Short-circuit `AuthCache::get_opt_job_authed` (the resolver behind
`get_authed`) to the admin superadmin in no-auth mode, so any direct cache
caller resolves without a real token. Single-source the mode check and the
synthetic identity via `is_no_auth()` / `no_auth_admin_authed()` and reuse
them across the extractor, resolver, and login paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* revert(auth): drop the NO_AUTH dismissable UI banner
The in-app banner added a GET /api/settings/no_auth_banner request to every
instance load for little benefit. The startup log warning already surfaces
that auth is bypassed to operators, so drop the banner, its endpoint, and the
no_auth_banner_dismissed global setting entirely.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: read chat drafts via own-draft route so drawer-kind drafts deploy
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: cover trigger and resource chat-draft read/deploy regressions
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: cover non-secret variable chat-draft read/deploy regression
Completes the drawer-kind matrix from the review notes on #9913: schedule,
trigger, and resource already had full write→read→deploy regressions; this
adds the variable one (non-secret — the secret flow deploys through the
ephemeral in-memory value and is pinned by the existing ephemeral tests).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ai_evals): mock getOwnDraft so eval draft hydration stays in-memory
The frontend eval adapter intercepts DraftService for benchmark workspaces,
but only updateDraft/getDraftForUser/listDrafts. Global eval output
collection hydrates draft values through getGlobalDraft, which reads via
getOwnDraft — so draft-producing global cases fell through to the real
generated client instead of the in-memory benchmark store. Adds a
getBenchmarkOwnDraft helper (null on miss, mirroring the 200/null route
semantics), wires it into the adapter mock, and pins it in
mockBackendDrafts.test.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
* fix(ci): gate auto-review on non-fork PR not author_association (skips private members)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ci): authorize private org members for command workflows via app-token gate
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* wip: partial work before earlyoom-recovery relaunch
* fix(pipelines): scaffold the strftime {partition} filter idiom (frontend-only)
The DuckDB materialize scaffold and the AI pipeline prompt now teach the
grain-agnostic `WHERE strftime(<ts_col>, '<fmt>') = {partition}` filter instead
of the naive `= TIMESTAMP {partition}` cast. `{partition}` substitutes to the
partition IDENTITY string (`2026-07-05T23`, `2026-W27`, `2026-07`), which is not
a valid DuckDB TIMESTAMP literal for any non-daily grain — so the naive form
raises a `Conversion Error` for hourly/weekly/monthly (only daily parses).
Adds a frontend unit test asserting the hourly scaffold emits the strftime
idiom (`%Y-%m-%dT%H`) for every grain and never scaffolds the naive TIMESTAMP
cast as executable SQL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipelines): scope strftime partition idiom to time grains
Review nit: `dynamic` partitioning's identity is a caller-supplied key, not a
timestamp, so `strftime` doesn't apply. Scope the scaffold + AI prompt claim to
time grains and add a `dynamic` example that filters on the user's own key.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(pipelines): wm_partition macro for grain-agnostic partition filters
The materialize runtime now injects a `wm_partition(ts)` temp macro as the first
setup statement of a time-partitioned script, so filtering the source to the
active slice is one grain-agnostic line — `WHERE wm_partition(<ts_col>) =
{partition}` — instead of a hand-written `strftime` format the author must keep
in lockstep with the resolver, or the `= TIMESTAMP {partition}` cast that only
parses for daily and Conversion-Errors for hourly/weekly/monthly.
The macro's format comes from `PartitionKind::default_time_format` in
windmill-parser, the same source the EE resolver reads to stamp the `{partition}`
identity, so the two can't drift. `dynamic` partitions get no macro (their
identity is a caller-supplied key → `WHERE <key_col> = {partition}`).
Replaces the earlier 9-line strftime comment block in the scaffold with the
single macro line; AI pipeline prompt and design doc updated to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(pipelines): verify wm_partition strftime parity vs chrono through real DuckDB
Runs the bundled DuckDB engine in-memory and asserts strftime renders every
grain format (daily/hourly/weekly `%G-W%V`/monthly) byte-for-byte identically to
chrono — the engine the resolver uses to stamp the `{partition}` identity —
across ISO-week year boundaries (2027-01-01 → 2026-W53 etc.). Also proves the
injected `wm_partition` macro buckets the whole slice and that the naive
`TIMESTAMP '<weekly|monthly identity>'` cast Conversion-Errors.
Closes the one cross-engine assumption the pure-Rust/frontend tests couldn't
reach (flagged by CI review for weekly ISO-week rendering).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 0de2412ff0734b11e12ba378c9bcc373ff9ae800
This commit updates the EE repository reference after PR #649 was merged in windmill-ee-private.
Previous ee-repo-ref: ad6c6685689d7741058e7d2c9ecbe95d982e6268
New ee-repo-ref: 0de2412ff0734b11e12ba378c9bcc373ff9ae800
Automated by sync-ee-ref workflow.
* fix(pipelines): classify CREATE TEMP MACRO as a DuckDB prepare-path setup statement
The FFI prepare/diagnostics pass only EXECUTES statements recognized by
is_setup_statement (ATTACH/USE/INSTALL/…); everything else is merely prepared.
`CREATE [OR REPLACE] TEMP MACRO` wasn't recognized, so on a `-- prepare` run of a
partitioned materialize the injected `wm_partition` macro was never created on
the connection, and the later generated `CREATE TABLE … SELECT … WHERE
wm_partition(...)` failed to bind ("function does not exist"). The same latent
gap affected the workspace-macro splicer, which injects TEMP MACRO blocks too.
Classify CREATE [OR REPLACE] TEMP|TEMPORARY MACRO as setup so it's executed
before dependent blocks and excluded from the PrepareQueryResult count
(persistent CREATE MACRO stays a user statement). Adds a prepare-path test that
fails without the fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat(pipelines): require data uploads before running a pipeline
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipelines): require every S3Object filled for data-upload readiness
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(pipelines): Run pipeline defaults schedule-triggered scripts to their schedule args
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: correct why only schedules default their args in Run pipeline
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipelines): close cascade double-start race and gate data-upload readiness on full-schema validity
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: expose ScriptEditor validity via callback, not banned bindable-with-default
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* ci for broken links + fix broken links
* ci: replace expiring-PAT org membership gate with author_association
The shared check-org-membership.yml reusable workflow authenticated to the
GitHub API with the ORG_ACCESS_TOKEN PAT to confirm org membership. That PAT
expired ~1 year after issuance, so the API could no longer see private org
members and check-membership emitted is_member=false — silently skipping every
auto-review, command-triggered review, /ai, /plan, and git-command job while
still reporting success.
Gate on the event payload's author_association (OWNER/MEMBER/COLLABORATOR)
instead, which comes from the built-in GITHUB_TOKEN and never expires. The
trusted internal bot and existing draft/fork/command guards are preserved; the
workflow_call paths stay open as trusted upstream. Deletes the now-unused
reusable workflow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): chat-scoped unified session changes bar
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(frontend): drop diff-baseline toggle, show natural per-row diffs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): carry Draft marker to expanded raw-app file rows
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): show raw-app Draft badge once at tree root, not per file
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(frontend): reuse shared DraftBadge in session diff drawer
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): show draft-author avatars in session diff badge, icon-only in sidebar
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): keep badge pill around avatar in icon-only DraftBadge
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): small draft marker = indigo pen + avatar; correct itemKind label
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(frontend): drop package-lock churn from merge (match origin/main)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): dedup diff-button count for legacy fork sessions; test mask helper
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): collapsible session diff panel + per-row open-diff action
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): shared sessionDeployModel for review & deploy (S1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): model-driven session review drawer, deploy inert (S2)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): wire session deploy + on-behalf/conflict gating (S3)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): behind banner + Update fork + deployment request (S4)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): session changes dock opens drawer by filter (S5)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): session review UI polish (badge, dock, In parent, tree width)
- draft rows show only the avatar DraftBadge, not a duplicate state pill
- drop redundant dock Review button (same as "N to review")
- rename Done -> In parent with a "deployed in parent workspace" tooltip
- widen the file tree
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): existence-gate In parent rows; Badge filters; badge hover
- drop discarded mask-only items from the In-parent segment (existence check)
- use the Badge component for the drawer filter segments and the changes dock
- soften the blue Badge hover (blue-50 base was jumping to blue-200)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): polish session diff drawer (layout, badges, actions)
- remove empty fork-banner gap; uniform sidebar tree padding
- full-bleed diff list: drop card borders/side padding, separators between items
- clamp tree x-overflow; right-align tree badges (min-w-0 on the row button)
- brand-compliant selected filter badges; smaller draft badge
- hide per-row open-diff button when the panel is open
- rename "Delete draft" to "Discard draft" (destructive); remove header Review button
- larger sm deploy/discard action buttons; remove per-item diff-content collapse
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): session Edits dock — deploy gating + change-op tracking
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): session bar per-status badges; drop change-op tracking
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(frontend): unwrap raw apps into per-file tree in session diff sidebar
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(frontend): dot-parcours pipeline (badge-derived, melt tooltip) + discard confirm
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): behind-only session item reads as deployed, not bare
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* style(frontend): even sidebar tree margins; gutter-aware right padding
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): pass chat id as from_session; wire deploying flag
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): session diff drawer scroll-to-flush, ordering, spacer, deploy gating
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): preserve chat mask on compact; guard stale existence checks; clear poll timers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): refresh bar after drawer deploys; conflict hint over chip; plain conflict badge
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* style(frontend): diff drawer card layout with flash ring and aligned insets
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): hide stale deployed chip once row status badge reads deployed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(frontend): session dock to two states; drop parent deploy
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(frontend): staged deploy animation in session edits drawer
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(frontend): stale-draft warning in session edits drawer
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): keep chat mask honest on deploy and discard
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): map trigger_email deploy kind; serialize mask persists
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): reset mask on new chat; close review-flagged races
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* style(frontend): rename session drawer title to Edited during session
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): keep mask persist queue alive after a failed save
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): sync session chatId on chat rotation; gate deploy on canWrite
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(frontend): keep compare handoff for deletion-only session edits
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): hold deploy success beat across re-keyed rows
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `duckdb` bundled feature compiles the whole DuckDB C++ library from
source (~2min), which dominates the FFI crate's build. A fresh git
worktree had an empty target dir and paid that cost every time.
build_dev.sh now builds into a per-user cache shared across worktrees,
keyed by Cargo.lock + build.rs so distinct DuckDB versions don't collide.
Uncommitted changes to the crate source fall back to an isolated
per-worktree ./target so active FFI development neither disturbs nor is
disturbed by the shared cache. Add a crate .gitignore for /target, and
note the shared cache in the AGENTS.md / backend CLAUDE.md build steps.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: resolve extensionless bun relative imports on windows loader
* fix: resolve local module probe against importer dir not job root
* fix: keep node_modules-internal relative imports out of windmill resolver on windows
* fix(cli): emit HD-1 test_edges + HD-2 scd2 _current write in --local pipeline graph
Close the remaining local-vs-deployed graph parity gaps in `wmill pipeline
show <folder> --local` so it matches the deployed graph (backend
`asset_graph`, windmill-api-assets):
- HD-1 `test_edges`: synthesize ordering-only producer → tested-script edges
from parsed `// data_test` annotations. A `relationships` test references
its `to_path` asset; a custom `// data_test <script>` resolves best-effort
against that script's parsed reads. Each referenced asset is resolved to its
in-pipeline producer via the write edges; self-edges and producer-less
(external) assets are dropped — mirroring the backend set semantics.
Routed through the asset node in boundedCascade's lineage DAG (asset →
tested script) so a cold/bounded cascade orders the referenced dimension
first, matching the frontend.
- HD-2 scd2 `<dim>_current` companion write: a managed `// materialize …
history` (scd2 && !manual) also produces a `<dim>_current` view. Register it
as a second write edge and mark the asset `derived_from` its base dimension,
so a consumer reading only the view links back to the producer instead of
orphaning. Gated exactly like the backend `MaterializeSpec::write_targets` /
`scd2_current_target`.
The pinned `windmill-parser-wasm-asset` (1.740.0) predates the `scd2`
materialize flag, so `buildLocalPipelineGraph` takes an injectable parser and
the HD-2 test injects one that re-adds `scd2` for a `history` materialize —
exercising the already-shipped companion-write branch until a wasm carrying
`scd2` is republished (cf. #9926).
Extends cli/test/pipeline_local_graph_unit.test.ts with HD-1 (relationships,
no-producer, self-test, custom) and HD-2 coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(cli): pin windmill-parser-wasm-asset 1.749.0, drop HD-2 test parser seam
Now that windmill-parser-wasm-asset 1.749.0 (which serializes the `scd2`
materialize flag) is published, bump the CLI pin and retire the temporary
injection seam:
- Remove the `infer?` parameter from `buildLocalPipelineGraph`; it always uses
the wasm-backed `inferScriptAssets` again.
- The HD-2 `<dim>_current` companion-write test drives the real wasm directly
(drops the `inferWithScd2` wrapper that re-added `scd2` against the pinned
1.740.0 build).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(frontend): pin windmill-parser-wasm-asset 1.749.0 to match CLI
Restore the CLI↔frontend lockstep on the asset parser wasm broken by the
previous commit: every other windmill-parser-wasm-* package is pinned to the
same version in both cli/package.json and frontend/package.json, so keep the
asset parser aligned too. The frontend derives materialize/scd2 from its own
TS annotation parser (`parsePipelineAnnotations`), so this bump only affects
body asset inference in the live graph — moving it in step with the CLI
`--local` graph and the deployed backend parser.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(pipelines): make node & pipeline-level run affordances always visible
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipelines): don't leave node Run disabled by stale form validity
The always-visible node Run button read `isValid` directly, but that flag is
only meaningful while PipelineRunForm is mounted to set it. On a same-path
re-resolve (the component is keyed on script.path) from an input-carrying
script to an input-less one, the form unmounts leaving `isValid=false`, which
wrongly kept the empty-args Run button disabled. Gate validity through a
`runValid` derived that is true whenever no form is rendered.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipelines): run with {} when node has no form, not stale args
The always-visible node Run button dispatched $state.snapshot(args)
unconditionally. `args` persists across a same-path re-resolve (component keyed
on script.path), so a script that once had inputs/partition args and is
re-resolved as input-less would run with the stale hidden args instead of {}.
Send {} whenever no form is rendered, matching the no-form run intent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): surface macro libraries in --local pipeline graph + make run --dry-run read-only
* fix(cli): resolve workspace-wide macro libraries in --local graph (shared libs outside the pipeline folder)
* fix(cli): macro-lib consumers + //-prefix parity in --local pipeline graph
Address Codex review P1s: (1) macro libraries that consume another library's
macros now produce lib->lib edges (any folder DuckDB script is a consumer, not
just // pipeline members) so an upstream provider node no longer disappears;
(2) parseMacroAnnotations accepts //, --, and # prefixes like the backend, so a
.duckdb.sql library headed with // macros is detected locally. Both edge
endpoints are forced into the node set. Verified byte-for-byte against deployed.
* fix(cli): exclude non-pipeline macro-consumer nodes from --local run selection
Address Codex P1: buildMacroEdges surfaces macro-consumer nodes (a DuckDB script
calling a macro but not marked // pipeline) for lineage display. Those have no
local file, so pipeline run --local must not treat them as manual roots — a
dry-run listed them and a real run failed resolving local content. Exclude any
--local graph node absent from localScripts (the previewable set) from starts and
selection, alongside the existing macro-library exclusion.
* fix(cli): reject display-only macro consumers in explicit --from (post-merge with #9945)
The mid-DAG --from feature (#9945, now on main) admits any autorun-able script
via validFromStarts/fromEligible, which was filtered only by macroLibPaths. A
non-// pipeline macro-consumer helper (a --local display node) therefore passed
--from eligibility and produced an empty plan. Filter fromEligible by the broader
notRunnablePaths too, and reject such a --from with a clear message instead of a
silent empty plan.
* chore(cli): remove NUL edge-key separator + refresh stale macro comments
Address Codex P2 nits: (1) the macro edge map packed (lib, consumer) into a
string with a literal NUL separator, which made localGraph.ts read as a binary
file to grep/rg — replace with a nested lib->consumer Map (no separator); (2)
comments claiming macro nodes/edges are 'deployed graph only' contradicted this
PR's local derivation — describe the code as it is.
* fix(cli): tag unused // pipeline + // macros libraries so --local run excludes them
Address Codex P1: the deployed builder sets 'macros' on any node whose path
provides macros (edge or not), so a // pipeline + // macros script with no
consumers is still recognized as definition-only. Local enrichment only tagged
edge providers, leaving an unused pipeline macro library as a bare runnable that
pipeline run --local would schedule as a manual root. Also tag any library whose
path is already a runnable; unused non-pipeline libraries stay suppressed.
* fix(pipelines): `// macros` takes precedence over `// pipeline` (a library is never a member)
A macro library is definition-only — its macros are injected into consumers and
running it is a no-op — so marking it `// pipeline` is meaningless and only
produced a confusing state (an unused pipeline macro library appearing as a
manual root). Make `// macros` win: parse_pipeline_annotations forces in_pipeline
false when macros is set. Mirrored in all three parsers that must agree — the Rust
canonical parser (drives deploy membership), the frontend TS parser (live graph),
and the CLI local graph (pinned wasm still reports in_pipeline, so precedence is
applied when skipping members). Shared parity fixture + unit tests on each side.
* docs(cli): trim narrative comment blocks to non-obvious constraints
Address Codex P2: duckdbMacros.ts opened with a ~19-line narrative block whose
parity rationale belongs in the PR description; reduce to the two real constraints
(keep in lockstep with duckdb_macros.rs; dynamic-SQL calls need // use). Per the
AGENTS.md comment policy.
* fix(cli): model macro libraries as pipeline members, matching the deployed graph
Reverts the parser-precedence approach (b398b69): the backend deliberately marks
EVERY macro library auto_kind='pipeline' (scripts.rs:1474, macro_lib_defs), so a
macro library IS a graph member — the // pipeline marker is redundant, not
authoritative. Precedence was a no-op on deploy while diverging the CLI/frontend.
Instead mirror reality in the CLI local graph: an in-folder // macros library is a
member node (in_pipeline=true, with signatures) whether used or not; its // use is
processed (it's a member) so a library that reaches another only via dynamic SQL
still gets the via_use lib->lib edge (fixes the missing-edge case); an out-of-folder
library referenced by an in-folder consumer is a non-member provider node. Macro
libraries stay excluded from runs (via macros) and from the previewable scripts set.
Verified byte-for-byte (incl. in_pipeline) against the deployed graph: unused
in-folder lib, lexical lib->lib chain, // use dynamic-SQL lib->lib, out-of-folder
shared lib.
* fix(pipelines): canonicalize S3 asset keys so SDK writes and DuckDB reads connect
The SDK object forms — TS `writeS3File({s3:"exports/x"})` and Python
`write_s3_file(S3Object(s3="exports/x"))` — resolve to the URI `s3:///exports/x`
(empty default storage), whose parsed asset path was `/exports/x` (leading
slash). DuckDB `read_csv('s3://exports/x')` and the `// on s3://exports/x`
trigger form yielded the bare `exports/x`. The same object thus produced two
asset identities, so a DuckDB consumer never connected to a TS/Python producer
in the pipeline graph.
`parse_asset_syntax` (shared by the native backend parsers and the wasm parser
that drives `frontend/src/lib/infer.ts` and the CLI `localGraph`) now strips a
single leading slash from S3 paths, so `s3:///key`, `s3://storage/key`, DuckDB
`s3://…`, and `// on` all canonicalize to one key. Both deploy-time inference
and editor/CLI inference agree, and the producer's write edge and the
consumer's read/trigger edge share a node.
Only one leading slash is stripped, so `s3:///` triple-slash default-storage
keys collapse to the bare key while Hive-partition keys
(`s3://bucket/y=2024/f.parquet`) and explicit-storage `s3://storage/key` paths
are untouched. Non-S3 asset kinds (res://, ducklake://, …) keep their paths
verbatim.
Note: existing deployed pipelines that recorded `/key` paths need a redeploy to
pick up the canonical `key`; the fix is forward-consistent for anything parsed
after this change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(pipelines): mark S3 asset-path normalization (item 6) resolved
The open-issues list still flagged the SDK-form leading-slash vs bare-URI
no-slash mismatch as "Still open", contradicting the fix in this PR. Mark it
resolved to match the updated Language-coverage prose.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs+test(pipelines): disclose S3 explicit-storage vs default-storage-nested-key aliasing
Collapsing to one canonical key means `s3://storage/key` (explicit storage) and
`s3:///storage/key` (default-storage nested key) now alias to the same node
`storage/key`, though they name different objects. Low-probability (needs a
storage config named to match a default-storage prefix) and inherent to a
best-effort lineage graph that doesn't split the first segment as a storage
name, but previously undisclosed. Document the tradeoff and pin the intended
aliasing with a test so it's intentional, not a latent surprise.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipelines): mirror S3 leading-slash strip in frontend live-preview parser
The pipeline graph live preview parses `// on` annotations client-side via the
hand-written `parsePipelineAnnotations.ts` (a TS mirror of the Rust annotation
scanner), NOT the wasm parser. Its `parseAssetSyntax` still returned the raw
suffix, so `// on s3:///exports/x` yielded `/exports/x` while the deploy-time
and wasm parsers now canonicalize to `exports/x`. `resolveGraph` synthesizes
trigger edges from that path, so the browser preview could still render
disconnected `/exports/x` and `exports/x` nodes for the exact triple-slash case
this PR fixes at deploy time.
Mirror the S3-only single-leading-slash strip in the TS parser and extend the
shared parity fixture corpus (run by both the Rust and TS parity suites) with
the triple-slash trigger case, so Rust/TS drift on this is now caught.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipelines): seed slashless S3 template asset paths to match canonical key
`autoOutputAsset` seeded new S3 template outputs with a leading slash
(`/pipelines/…`), which the old parser required to match `s3:///key` writes.
This PR made `parse_asset_syntax` strip that slash, so the seeded draft asset
(stored as `outputAssets`, used by `resolveGraph` for inactive-draft node
identity) no longer matched the body-inferred identity `pipelines/…` — the live
preview could render a duplicate `/pipelines/…` node and a phantom post-deploy
drift warning.
Seed the canonical slashless key instead, and switch the DuckDB body's S3 URIs
from `s3://${path}` to `s3:///${path}` so the generated runtime URI stays the
triple-slash default-storage form byte-for-byte (the SDK sites already build
`s3:///` + bare key). Add a pure-logic parity test asserting, for every
language and S3 output kind, that the seeded asset path is slashless and that
every S3 URI the generated body emits is triple-slash and canonicalizes back to
that seeded path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipelines): canonicalize S3 keys in CLI + frontend bounded-cascade resolvers
Two more hand-written S3-URI sites returned the raw suffix, so `s3:///exports/x`
stayed `/exports/x` while native/wasm parsers now canonicalize to `exports/x`:
- `cli/src/commands/pipeline/localGraph.ts` — the no-wasm fallback `// on`
scanner (go/bash/ruby). A fallback consumer's `// on s3:///x` would not
connect to a wasm-inferred `x` producer in `wmill pipeline show/run --local`.
- `boundedCascade.ts` `assetUriToNodeId` (duplicated in the CLI and the frontend
AssetGraph engines, kept in sync) — `--to s3:///exports/x` / a cascade bound
token would not resolve against the canonical graph node `s3object:exports/x`.
`resolveToken` delegates here, so it is covered too.
Mirror the S3-only single-leading-slash strip in all three, and add `s3:///`
tests to the CLI local-graph fallback suite and both bounded-cascade suites
(explicit-storage and Hive-partition keys asserted untouched).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(pipelines): phrase S3 template test comment as a current invariant
Describe the slashless-seed requirement as the invariant it is, not as change
history, per the AGENTS.md "describe the code as it is" rule.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipelines): strip all leading slashes from S3 keys so trigger refs round-trip
`parse_asset_syntax` stripped only one leading slash, so `S3Object(s3="/x")` —
which resolves to the quad-slash URI `s3:////x` — parsed to path `/x`. But
`trigger_spec_to_row` rebuilds a stored trigger ref as `s3://<path>` =
`s3:///x`, which `parse_asset_trigger_ref` then parses back to `x`. The
producer recorded `/x` while its consumer trigger resolved to `x` → a broken
edge. The same asymmetry affects every `s3://`+path reconstruction site
(backend refs, frontend `assetUri`, page refs) whenever a path starts with `/`.
Strip ALL leading slashes so a canonical S3 path never starts with `/`; naive
`prefix + path` reconstruction then round-trips everywhere. Applied uniformly
across all six S3-URI sites (Rust `parse_asset_syntax`, the TS live-preview
parser, template `s3Key`, and the frontend+CLI `assetUriToNodeId` and CLI
fallback scanner). The pathological leading-slash key collapses to the bare key
— acceptable for a best-effort lineage graph that never split storage anyway.
Tests: a windmill-common round-trip test (parse → trigger_spec_to_row →
parse_asset_trigger_ref) over every URI form incl. the quad-slash case; a
`s3:////x` shared parity fixture (Rust + TS); and quad-slash assertions in the
Rust parser test and both bounded-cascade suites.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(pipelines): align S3 template parity helper with strip-all canonicalization
The template seed/body parity test's `canonicalS3Key` helper (and its comment)
still stripped a single leading slash, so it no longer mirrored the parser it
claims to pin. Strip all leading slashes to match `parse_asset_syntax` and the
frontend/CLI mirrors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(pipelines): mid-DAG selective execution (dbt `model+`) for pipeline runs
Relax the root-only constraint on bounded-cascade starts so `--from` can name
any node in a pipeline — not just a schedule/manual root. A mid-DAG start runs
that node plus its transitive downstream and never re-runs upstream, giving
dbt's most common gesture (`dbt run --select model+`) a direct form:
wmill pipeline run f/orders --from fct_orders_daily
Previously this errored with "Starts must be schedule-triggered or manual
roots". The bounded-run engine already computed downstream/path-between sets
generically; only the eligibility gate was root-only.
- Shared engine (`boundedCascade.ts`, CLI + frontend mirror): add
`validFromStarts` — every autorun-able script (roots AND mid-DAG asset
subscribers / pure readers), excluding only event/input-only handlers
(kafka/mqtt/…/webhook/data_upload) that can't run with empty args.
- CLI: `--from` accepts any `validFromStarts` node; asset `--from` and
non-autorun handlers still rejected (the latter runnable via `--upload`). An
explicit mid-DAG start is protected from the barrier cut. Help text + regenerated
system_prompts describe the new surface.
- Frontend graph UI parity: any node with downstream now offers "Run + downstream…"
(was roots-only). With no end picked the bounded-run bar runs the full downstream
closure (`model+`); picking end(s) still bounds the path-between set.
- Unit tests for the new selection semantics in both engines.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipelines): address CI review — scheduled-root --from regression + pick-mode barrier parity
Codex review findings on #9945:
- P1: explicit `--from` rejected a scheduled root that also carries a secondary
non-autorun trigger (e.g. `// on schedule` + `// on data_upload`), even though
it stays a valid IMPLICIT start. `validFromStarts` excluded anything in
`nonAutorunTriggerScripts`; now it unions in `validStarts` (which lets the
schedule identity win over the secondary trigger), so a scheduled root is
`--from`-eligible in both CLI and the graph UI. Regression tests added in both
engines.
- P2: bounded-pick mode built `eligible` (pickable end bounds) from raw
`descendants`, so an event handler — or a node only reachable through one —
could be clicked as an end yet be silently dropped from the barrier-cut run.
`eligible` is now the barrier-cut closure, so those nodes are dimmed and
non-pickable. The highlighted `bounded` ring now also reflects the actual
(barrier-cut) run set, including the no-ends "Run + downstream" case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipelines): frontend barrier set must exclude all valid roots, not just the picked start
Codex review follow-up: the frontend `boundReachable` barrier set only protected
the picked start (`id !== boundPickStart`), while the CLI protects every valid
root (`!starts.has(id)`). So a scheduled root that also carries an event trigger,
reached downstream from another start, was wrongly treated as a barrier — the UI
dimmed/skipped it and its downstream, diverging from the CLI run set.
Exclude `validStarts` from the barrier set too (a scheduled/manual root runs on
its own identity even with a secondary event trigger). Regression test asserts a
scheduled-event root and its downstream stay reachable from an upstream start,
and that the naive (start-only) barrier set would have dropped them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipelines): frontend must exclude webhook/data_upload as mid-DAG autorun starts
Codex review follow-up: the frontend `validFromStarts` only excluded
`EVENT_TRIGGER_KINDS`, so a mid-DAG `webhook`/`data_upload` subscriber was added
by the new eligibility loop — the UI would offer "Run + downstream" and launch it
with empty args (no uploaded S3Object / webhook body). The CLI mirror already
excludes these input-only kinds.
Add a frontend `NON_AUTORUN_TRIGGER_KINDS` (event kinds + webhook + data_upload),
mirroring the CLI, and use it in both `validFromStarts` (exclude such mid-DAG
handlers from starts) and `nonAutorunTriggerScripts` (cut them as barriers).
When the marker is visible (editor overlay / draft) these are now handled
exactly as the CLI does; the deployed-graph blind spot (no webhook/data_upload
rows) remains the documented pre-existing `validStarts` limitation.
Regression test: a `data_upload`/`webhook` mid-DAG subscriber is not an eligible
start and is barrier-cut (with its exclusive downstream) when running from an
upstream root.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(pipelines): partition run-arg picker + first-run setup signpost
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipelines): honor partition start= anchor in picker default and per-input upstream hint
Addresses CI review (Codex):
- P1: never seed/offer a pre-start bucket — the worker takes an explicit partition arg verbatim, so seeding today's bucket before start= would materialize early. defaultBucket now clamps to the start bucket and drops pre-start recent-missing chips; a hint explains the start anchor.
- P2: upstream-missing hint checks each partitioned ducklake input separately instead of unioning, so a fan-in where one input has the bucket no longer masks another that lacks it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipelines): reseed partition picker on header change and fail safe on bad tz/start
Addresses second CI review (Codex):
- P1: run form is now keyed on the parsed partition spec as well as the schema, so editing the // partitioned header (same schema) remounts the picker and reseeds/re-strips instead of keeping a stale bucket that re-bypasses the start anchor.
- P1: malformed metadata is fail-safe in parity with the backend — invalid tz= no longer throws in Intl (falls back to UTC for display), invalid start= (e.g. 2026-02-31) is rejected via round-trip check, and neither auto-seeds an explicit partition (which would bypass the worker's own tz/start validation). A warning hint points at the header.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipelines): pipeline-level run control, tables label, data-test rollback + fork badges
- Add always-visible "Run pipeline" header control (edit mode) that runs
every script in dependency order via the bounded-cascade engine, so a run
no longer requires hovering a node's play button.
- Header summary counts ducklake/datatable assets as "tables" (and s3object
as "files") instead of the raw kind, collapsing shared nouns.
- Surface a data-test outcome badge on guarded asset nodes: EE shows a
rolled-back (previous version left live) state, CE shows published-despite-
failure — driven by the producer's last run state and the edition.
- Make the fork data-environment marker a prominent labeled chip
(⑂ fork / ↗ parent) instead of a bare icon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipelines): address CI review — scope Run pipeline to members, anchor guard badge, spin loader
- Run pipeline now filters to `in_pipeline` script runnables, so it never
launches dependency-only endpoints the graph shows for context (macro
libraries, custom data-test scripts, out-of-folder producers).
- Data-test guard badge only attaches to the producer's declared
`// materialize` target, so a multi-output producer no longer badges its
other ducklake writes.
- Spin the Loader2 icon in the "Run pipeline" button while a run is in
progress (startIcon classes), matching every other loading affordance.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipelines): data-test badge copy speaks to write policy, not failure cause
producerFailed is a generic job-failure signal, so the failed-state tooltip
no longer claims the run "failed its data tests" (it could be a runtime/worker
error). It now states the edition's behavior on any failed materialize: EE
rolls back (previous version left live), CE may leave a failing write live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipelines): Run pipeline keeps independent branches running after a failure
runSelection used a single global fail-fast flag, so once any node failed it
refused to schedule *any* newly-ready node — a failure in one branch could
strand an unrelated healthy branch as 'skipped' depending on job timing. Now a
failure poisons only its transitive descendants; independent branches finish.
Add regression tests: independent-branch-survives-failure and join-node-skipped
-when-one-upstream-fails.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
`ATTACH 'datatable://main'` (or any datatable schema/executor path) failed
with a bare "datatable main not found", giving the user no way forward — the
datatable substrate has no auto-provisioning like a DuckLake catalog, so the
fix is always to create one in workspace settings, but nothing said so.
`get_datatable_resource_from_db_unchecked` now returns a NotFound error that
lists the workspace's configured data tables (to catch typos) and points at
the "Data tables" settings tab, noting `main` is the default name used by
`datatable://main`. The message bubbles up wherever the resolver is called
(pipeline ATTACH, schema fetch, postgres executor, agent HTTP endpoint).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipelines): link SCD2 <dim>_current view to its producer across all graph surfaces
An SCD2 producer (`// materialize … history`) creates the base table AND a
`<dim>_current` view at runtime. The deploy path already registered both writes,
but the CLI `--local` graph and the frontend live-editor graph only emitted the
base write, so a consumer reading only `<dim>_current` orphaned there. Centralize
the companion derivation in `MaterializeSpec::write_targets` /
`scd2_current_target` (+ TS `scd2CurrentTargetPath` mirror), emit the `_current`
write in every surface, and mark the companion node `derived_from` the base so the
canvas renders it as a derived "current view" instead of an unrelated table.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipelines): keep scd2 _current write edge when editing a saved producer
Addresses Codex CI review (P1): opening a deployed scd2 materialize producer for
editing dropped its persisted `<dim>_current` write edge. `liveRefKeys` (the set
of asset keys a saved-script edit preserves against stale-filtering) only added
the base materialize target, so the companion `_current` write was judged stale
and filtered — orphaning consumers of only the view mid-edit. Add
`scd2CurrentTargetPath(m)` to `liveRefKeys` too; covered by a new saved-edit test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipelines): order data_test relationships refs before the tested script in a cascade
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipelines): key custom-test reads by (usage_kind, path) to avoid same-path flow collisions
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Custom `// data_test <path>` scripts must be a single SELECT reading the
freshly-materialized target via the internal `_wm_target.<table>` alias —
neither was documented or scaffolded. Make the codegen errors name the exact
violation (multi-statement, non-SELECT, wrong alias, empty) and append a
copyable `SELECT * FROM _wm_target.<table> WHERE <condition>` example. Add a
DuckDB-only 'Data test' pipeline output kind that scaffolds that starter body.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two correctness/validation improvements to managed materialization:
1. A keyed `merge` (`key=<col>`) is delete-by-key + insert-all and does NOT
deduplicate its source, so two incoming rows sharing a key both landed
under that key — silently breaking the one-row-per-key contract. Codegen
now emits an in-transaction guard (same `error(...)` shape as the schema
-drift guard) that fails the run when the SELECT returns more than one row
for a non-NULL key, naming the key. Authors deduplicate in the SELECT or
switch to `append`. NULL keys are exempt, matching the delete's `IN (...)`
scope.
2. The two SCD2 misconfigurations that were only caught at run time — `history`
without `key=`, and `history` + `// partitioned` — now fail fast at deploy
via a shared `MaterializeSpec::validate`, called from `create_script_internal`.
The DuckDB executor keeps the same check as a safety net for preview/test
runs that never deploy (shared message, no drift).
Adds unit tests for the merge guard codegen and for `validate` (all four
cases), and updates docs/ducklake-materialization.md and docs/pipelines-vs-dbt.md.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(object-storage): remove 20-file bucket-browser cap in CE
The Community Edition build rejected the object-storage `list_stored_files`
endpoint with an error once a workspace bucket held more than 20 objects,
making the bucket browser unusable on larger buckets. The listing already
collects up to `max_keys` objects, so the hard cap was purely a gate.
Drops the CE listing cap (in the EE-symlinked `job_helpers_ee.rs`, tracked
in the companion windmill-ee-private PR) and removes the now-inaccurate
sentence from the workspace object-storage settings banner. The 10 GiB
total-storage write quota remains as the intentional CE limit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 4bd7b73dcef1b77dc2866bc58e0c86962559495c
This commit updates the EE repository reference after PR #648 was merged in windmill-ee-private.
Previous ee-repo-ref: aa14d0724216030948c2f575bcc19c0e6e0476a7
New ee-repo-ref: 4bd7b73dcef1b77dc2866bc58e0c86962559495c
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix(duckdb): render FFI errors with real newlines and no stray quoting
The DuckDB FFI returns errors as ERROR <json-encoded-message>, so the
executor was surfacing the serde_json-escaped form (wrapping quotes,
literal \\n). Multi-line errors like the write-audit-publish data-test
breakdown were unreadable. Decode the JSON string back to the raw message
at both FFI error sites.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: tighten decode_ffi_error comment to the invariant
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipelines): clarify activity-window axis label + select failed node on cascade failure
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): clear active draft so failed-node focus is not masked
PipelineGraphEditor gives an open draft (activeDraftPath) priority over
selection via openScriptPath, so the cascade-failure focus set only selection
and stayed masked while a draft pane was open.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(pipelines): on_schema_change write guardrails + data_test deploy validation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: update ee-repo-ref to fa7ac11c1e0ab39e84a0c18973ba427a240933ca
This commit updates the EE repository reference after PR #647 was merged in windmill-ee-private.
Previous ee-repo-ref: bd23b2a904cb2e6554c7ff209ff8adb9d91775d1
New ee-repo-ref: fa7ac11c1e0ab39e84a0c18973ba427a240933ca
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix(cli): publish all windmill-parser-wasm-* deps so local pipeline graph keeps write edges
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* style: trim explanatory comment blocks to core constraints
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(pipelines): fork-scoped ducklake namespaces with read-defer to parent
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(pipelines): fork graph indicator + fork ducklake namespace cleanup endpoint
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(pipelines): fork_views-keyed view transition, fork lineage clone, design doc
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): review hardening - fork DATA_PATH last-wins, registry cache TTL, defer tests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(pipelines): per-lake isolated/shared choice at fork creation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): chain-aware defer discovery + per-location fork namespace registry
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): lake-scoped fork schemas, catalog identity in registry, chain-aware graph chips
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): cleanup deletes fork data from the registered storage identity
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): collapse fork data-path segment to one component (slash-safe ids)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): per-catalog ancestor checks, ancestor extra_args passthrough, test compile fix
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): invalidate fork ancestor-chain cache on lineage mutations
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): sweep descendant ancestor-chain caches on delete/reparent
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): run fork ducklake cleanup inline in delete_workspace
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): resolve fork cleanup credentials pre-commit, destroy post-commit
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): shared dev-workspace authz gate for namespace drop, invalidatable registration cache, segment-boundary delete filter
- extract require_prod_admin_for_dev_workspace, used by both delete_workspace
and drop_forked_ducklake_namespaces so the gates cannot drift
- key FORK_DUCKLAKE_REGISTERED per workspace and invalidate it in
cleanup_fork_ducklake_namespaces so a same-id fork recreated within the TTL
re-registers its namespaces
- filter listed object locations to the segment boundary before deletion
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): keep orphaned wm-fork-* workspaces ducklake-isolated
parent_workspace_id is ON DELETE SET NULL, so a fork can outlive its
parent with an empty ancestor chain while its cloned config still points
at the shared lake. Key the isolation gate on the wm-fork- prefix as well
as the chain (mirroring workspace_is_fork): orphaned forks get the write
redirect, registration and cleanup with zero ancestors (no defer), and
keep their 'fork' graph chips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): attach orphaned wm-fork-* ancestors at their fork namespace
Chain position alone classified the last ancestor as a root, but an
orphaned wm-fork-* ancestor (its own parent deleted, SET NULL) ends the
chain the same way while its data lives in its fork namespace — its
descendants' defer views bound the dead root's lake instead. Key the
root-vs-fork decision on the wm-fork- prefix too, matching the
resolution gate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): never inherit shared lake opt-out; durable cleanup ledger for failed fork deletions
- fork creation strips cloned fork_behavior stamps before applying the
request's shared_ducklakes list: sharing is a per-creation choice, a
fork of a shared fork defaults back to isolated
- fork_ducklake_namespace loses its ON DELETE CASCADE FK: rows are the
durable cleanup ledger and outlive the workspace when physical cleanup
fails post-commit; fork creation retries leftover rows for the reused
id and refuses to create while a metadata schema still cannot be
dropped (data-file leftovers alone are inert once the schema is gone
and are swept by the next successful same-prefix cleanup)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): make orphaned-namespace cleanup retries independent of deleted fork resources
- ledger rows gain a schema_dropped phase flag: set when the schema drop
succeeded but data cleanup failed, so later retries skip the schema
phase and need no catalog credentials at all; registration resets it
on re-attach (ON CONFLICT DO UPDATE) since attaching recreates the
schema
- retry-path $res: resolution falls back to the workspace being forked
(the deleted fork's resources were clones of a parent's); live paths
(delete_workspace prepare, drop endpoint) pass no fallback
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): fork tables from failed-after-commit runs stay fork-owned in defer and graph
A failed materialization must not disguise a physically existing fork
table as deferred: CREATE VIEW IF NOT EXISTS silently yields to the
table, so reads hit fork data while the graph claims parent defer.
- record_mat upsert preserves the last committed snapshot_id on failure
- defer discovery and graph chips treat fork rows with a committed
snapshot as fork-owned even when status is failed
- inspect_fork_catalog also lists live fork tables (same round trip) and
the defer list is filtered against them — covers rows recorded before
this fix and tables created by raw SQL
- drop stale FK-cascade wording in the design doc and sidebar comment
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(pipelines): fork-mode ducklake settings — per-lake isolated/shared chips + banner, fork_behavior round-trip
The workspace-settings ducklake editor had no fork awareness: no
reminder of each lake's isolated/shared choice and no warning about
what edits mean in a fork. It also rebuilt each lake explicitly on
save, silently dropping fork_behavior — any settings save in a shared
fork flipped the lake back to isolated.
- fork detection mirrors the backend gate (parent link or wm-fork- prefix)
- info banner explaining isolated vs shared semantics in a fork
- per-lake chip (emerald 'isolated' / amber 'shared with parent') with
tooltips, matching the pipeline graph chip colors
- fork_behavior added to DucklakeSettingsType and preserved through
convertDucklakeSettingsToBackend
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(pipelines): capture violating-row samples for data tests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): byte-accurate sample cap and leaf-level payload sanitize
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: bump ee-repo-ref to WAP guard probe adaptation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: WAP failures are counts-only — samples exist only on commit-then-test
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: qualify where sample row data appears — job result and failed-job log line
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: error handlers receive the full result incl. samples, like any failed job
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: update ee-repo-ref to 80d309edebb899e36a3bdcdf4ea73c4db070534d
This commit updates the EE repository reference after PR #646 was merged in windmill-ee-private.
Previous ee-repo-ref: 16e916bf11f26381920560b55771fce693e668c6
New ee-repo-ref: 80d309edebb899e36a3bdcdf4ea73c4db070534d
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat(pipelines): ingestion (EL) templates + docs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): review nits — draft collision guard, template-mode selection reset, invariant test
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): lead the insert menu with ingestion templates
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(pipelines): ingestion story as docs-only — drop editor template UI
The insert-menu template section mixed two selection grammars in one popover and confused more than it helped. The three E2E-verified example pipelines now live verbatim in docs/pipeline-ingestion.md; the Python bare-string S3 key fix in pipelineTemplates.ts stays.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(sdk): bare string S3 keys in py/ts clients + asset parsers
A plain string passed where an S3Object is expected is now a bare key in the default storage — previously the py client silently degraded it to s3="" (auto-generated key) and both asset parsers canonicalized it without the leading slash, splitting lineage. parseS3Object moves to s3Types.ts so it is unit-testable without the generated services. The pipeline template fix from the earlier commit is superseded (bare strings are the supported spelling again); docs examples flipped to bare keys.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(sdk): enforce s3:// URIs for string S3Object params
Bare strings now raise/throw with a hint pointing at the s3:///<key> spelling instead of being treated as keys (previous commit) or silently degrading to an empty key (original behavior). One string spelling everywhere: SDK calls, // on annotations, and DuckDB SQL all use s3:///<key>. TS regains the s3://-template-literal type; the asset parsers record no asset for a bare string (the call can only error); templates emit the URI form.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(pipelines): move ingestion (EL) guide to windmilldocs, keep design constraints
User-facing how-to (engine choice, cursor recipes, schema drift, worked examples) moves to windmilldocs core_concepts/63_pipelines (windmilldocs#1462); the repo keeps only the design constraints future feature work must not break, as a section of ducklake-materialization.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: regenerate system prompts after parse_s3_object docstring change
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): reject empty-key s3 URIs; align asset parsers with the runtime rule
Addresses CI review: s3:/// and s3://bucket/ now raise (an empty key would fall back to the auto-generated-key path the strict contract exists to prevent); the asset parsers' string branch applies the same valid-URI-with-non-empty-key rule so no R/W edge is recorded for a call that can only error (the generic URI-literal scan still records ambiguous access-None assets, by design); comments rephrased as current constraints per AGENTS.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(ducklake): scheduled lake maintenance (snapshot expiry, compaction, orphan cleanup)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ducklake): review fixes — starts_with not LIKE, CE license-lapse escape
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(ducklake): auth-contract docs + _unchecked rename per codex review
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(ducklake): move maintenance payload construction into EE module
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ducklake): fall through to script resolution for non-managed reserved-prefix schedules
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(ducklake): document accepted pre-existing-schedule limitation on the reserved prefix
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ducklake): CE save-off clears the managed schedule row and queued occurrence
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: update ee-repo-ref to 2fab310d4f50ed7c34857d69c9b854f4491bf217
This commit updates the EE repository reference after PR #645 was merged in windmill-ee-private.
Previous ee-repo-ref: fff1fd830a36beba732486f05941ec243cf6b640
New ee-repo-ref: 2fab310d4f50ed7c34857d69c9b854f4491bf217
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat(pipeline): write-audit-publish for materialization data tests (EE)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: EE worktree E0583 troubleshooting + duckdb feature check row
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: clarify EE symlink example (absolute target, EE repo layout)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipeline): move bootstrap DDL inside guarded WAP transaction
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(pipeline): move WAP guard SQL builder into EE, OSS keeps placement only
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: bump ee-repo-ref to EE branch rebased on EE main
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* style: reword test comment as current invariant per AGENTS.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: bump ee-repo-ref (EE module doc update)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(pipeline): OSS emits typed materialize plan, EE owns WAP transform
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: make rewrite assertion build-aware; refresh oss module doc
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: update ee-repo-ref to 7be0bad1a6d6b5c3a107c0a2cd4bf003c36ec34c
This commit updates the EE repository reference after PR #644 was merged in windmill-ee-private.
Previous ee-repo-ref: 63cabae75329429f647e01083936d70f8197dc9e
New ee-repo-ref: 7be0bad1a6d6b5c3a107c0a2cd4bf003c36ec34c
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat(ai-agent): support reasoning effort in AI agent workflow steps
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-agent): round-trip native Anthropic thinking blocks and fix DeepSeek/Mistral reasoning
Address review: native Anthropic now captures the signed thinking block during streaming and replays it before tool_use across iterations (prevents a 400 on multi-turn tool use). DeepSeek 'off' sends thinking:{type:disabled} instead of the rejected reasoning_effort:none, and Mistral drops temperature when reasoning is on.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-agent): move reasoning effort into the provider/model selector
Store reasoning_effort on ProviderConfig (next to the model) instead of a separate flow arg, and render the selector inside AIProviderPicker under the model dropdown. Add an explicit 'off' option on models that disable reasoning by omission (e.g. Claude), so reasoning can always be turned off from the UI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(ai-agent): use DropdownV2 for reasoning effort, matching copilot chat
Replace the Select combobox with the same DropdownV2 action-menu the copilot chat reasoning selector uses. Each option carries an action instead of a bound value, so click selection is unambiguous and there is no typeahead/sentinel-value mismatch on the off/default entries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(ai-agent): regenerate system prompts for ProviderConfig.reasoning_effort
Refresh system_prompts/auto-generated and cli skills.gen after adding reasoning_effort to the OpenFlow ProviderConfig schema (check-freshness).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-agent): clear stale reasoning effort on model change; dedup bedrock reasoning folding
Address cubic review: (P1) the reasoning picker now clears the stored effort when the newly selected model doesn't accept it (e.g. carrying 'xhigh' from Opus onto a model that tops out at 'high'), not only when the model can't reason at all. (P3) the proxy's accumulate_reasoning_delta now delegates to the shared bedrock_stream_event_to_reasoning_delta so worker and proxy folding can't drift.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-agent): stream reasoning summary and show a thinking affordance in flow chat
Add StreamingEvent::ReasoningTokenDelta, emitted from every worker reasoning path (Anthropic native thinking deltas, Bedrock, Gemini thought parts, OpenAI-compatible reasoning_content, OpenAI Responses reasoning_summary_text with summary:auto). The flow chat parses it and renders a collapsible 'Thinking' affordance on the assistant message (thinking tokens bill regardless of display, so surfacing the summary is billing-neutral).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-agent): persist streamed reasoning onto the finished chat message
Reasoning isn't stored server-side, so the completion re-poll (which swaps temp messages for the persisted ones) was dropping the streamed thinking summary. Carry it onto the final assistant message so the 'Thought process' affordance survives the run.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-agent): match flow-chat thinking box to the copilot chat reasoning UI
Replace the plain <details> thinking affordance with the same bordered, collapsible reasoning box the copilot chat uses (chevron + Brain/spinner + 'Thinking' header, markdown body, expand-while-streaming/collapse-on-answer).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-agent): attribute streamed reasoning per turn by content; drop duplicated comment
Address review: the completion-poll carry-over now matches each temp assistant turn's thinking summary to its persisted message by content, so a multi-turn response (reasoning -> tool call -> final answer) no longer misattributes an earlier turn's thinking to the final answer or drops intermediate turns. Also removes a leftover duplicated comment block above the AIReasoningEffortPicker effect.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-agent): address review round 2 (carry-over edges, off-token validity, aria, test)
cubic round 2: (1) reasoning carry-over now consumes temp turns in order verifying content, so identical/empty-content multi-turn responses attribute thinking correctly and reasoning-only turns aren't dropped; (2) the picker's stale-value check only accepts the off token when the model can actually disable reasoning; (3) add aria-expanded to the Thinking toggle; (4) add a test for the failed tool_result path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-agent): add bottom margin below the flow-chat thinking box
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-agent): don't request OpenAI reasoning summary, matching the copilot chat
OpenAI gates reasoning summaries behind org verification, so requesting summary: auto would 400 for unverified orgs. The copilot chat requests effort only and never asks for a summary; align the worker with it (reasoning: { effort }) and drop the now-unreachable summary-delta parsing. OpenAI reasoning no longer streams a summary in flow chat (consistent with the copilot); Anthropic/Bedrock/Gemini/DeepSeek reasoning display is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-agent): scope reasoning carry-over to newly persisted messages
cubic round 3: matching by content over the full history could attach a new turn's reasoning to an older message with identical text. Restrict eligible targets to the messages just fetched for this response (via afterSeq), so historical turns are never touched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-agent): carry reasoning newest-first instead of gating on the final poll
cubic round 4: gating the carry-over on the final poll's filteredResponse dropped reasoning for messages already fetched by an earlier streaming poll (their id is excluded by afterSeq). Walk persisted newest-first and consume the newest matching pending summary, stopping once summaries run out. This response's turns are always at the end, so they claim their own reasoning (P1) before older history is reached (P2), regardless of which poll persisted them.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(ai-agent): drop flow-chat reasoning display, keep backend + effort picker
The chat-side thinking box relied on non-deterministically matching streamed (ephemeral) reasoning back onto persisted messages, which kept spawning edge cases. Remove the flow-chat display entirely (ChatMessage box, FlowChatManager carry-over/threading, parseStreamDeltas reasoning) and keep the sound backend: per-provider reasoning-effort requests, thinking-block round-trips for tool calls, and ReasoningTokenDelta streaming. A display can be built on top later, deterministically (e.g. once the stream carries the persisted message id).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-agent): include reasoning_effort in default-config compare; document reasoning_token_delta
Codex/Pi nits: isSameAsStoredConfig now compares reasoning_effort so the 'use as personal default' toggle reflects effort-only changes; openflow streaming-events doc lists the reasoning_token_delta event (regenerated auto prompts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(pipelines): passive asset freshness tracking on the graph
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(pipelines): drop dead freshness-enforcement stub, document query ordering
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(pipelines): freshness watchdog (EE) — auto re-run stale producers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): watchdog review fixes — archived workspaces, badge kind parity, scan index
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): CI review — no singlestepflow in freshness, +N parity, completion-time fallback
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pipelines): CI review — history completedAt, freshness/asset trigger UI metadata
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: update ee-repo-ref to 6f5fe0f7f56696fbef5a8349da38496c32e71666
This commit updates the EE repository reference after PR #643 was merged in windmill-ee-private.
Previous ee-repo-ref: 1f13380354bf591ae25a2c20d36917534bcc5459
New ee-repo-ref: 6f5fe0f7f56696fbef5a8349da38496c32e71666
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat(pipelines): record upstream snapshot ids on cascade-dispatched jobs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor: batch upstream-snapshot lookup and memoize per subscriber
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(kafka): set https.ca.location=probe for OAUTHBEARER OIDC token endpoint
Bump EE ref to pull in the mod_ee.rs change that sets https.ca.location for
the Kafka OAUTHBEARER (OIDC) token endpoint HTTPS request.
EE companion: windmill-labs/windmill-ee-private#642
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 81d8a449effdc540a6e8810668dd5d4aea0c485a
This commit updates the EE repository reference after PR #642 was merged in windmill-ee-private.
Previous ee-repo-ref: a6761c3cbbf788c7273296f49bb0c39eef85afb9
New ee-repo-ref: 81d8a449effdc540a6e8810668dd5d4aea0c485a
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
* fix(frontend): prevent truncated ai chat tool-call arguments from bricking the session
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: heal empty tool-call arguments when replaying chat history
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: state sanitize invariant without drafting history
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(ai): route Azure Foundry Claude models via Anthropic Messages API
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai): keep explicit Azure OpenAI deployment base URLs intact
build_azure_openai_url only appends /openai/v1 for a bare resource root; any base with an explicit path (e.g. .../openai/deployments/<id>) is preserved. Adds a regression test and a unit test for usesAnthropicMessagesApi.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai): enable Claude extended thinking on Azure Foundry
Route azure_foundry+Claude through the Anthropic reasoning branch (adaptive thinking + output_config.effort) instead of the gpt/o gate, and recognize claude-sonnet-5. Live-verified: sonnet-5 and opus-4-8 on Foundry accept the low/medium/high/xhigh/max ladder and render summarized thinking.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
App values are persisted to a json column, which permits the JSON NUL
escape (backslash-u-0000), but are later converted to jsonb (e.g. a
workspace fork clone_apps, search indexing), which rejects it with
"unsupported Unicode escape sequence" -- silently making the app
un-forkable. The usual source is a binary file such as .DS_Store
accidentally bundled into a raw app file map.
A real NUL is unstorable in jsonb either way, and frontend code that
needs the character writes it as the source escape (which JSON-encodes
to an escaped backslash + literal u0000 and is left untouched), so rather
than hard-failing the save we strip genuine NULs and warn.
Add strip_null_chars and apply it at both app_version insert sites
(create_app_internal and update_app_internal, covering the regular and
raw create/update routes). It removes a genuine NUL escape (odd run of
backslashes before u0000) while preserving an even run. Returns a
borrowed Cow (no allocation) when the value is already clean. Covered by
unit tests.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(forks): clone only the current raw-app bundle, via server-side copy
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(forks): fall back to get+put when object-store copy is unsupported
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(s3_proxy): enforce CE 50MB upload cap on multipart uploads
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(s3): replace CE 50MB upload cap with 10GiB workspace storage quota
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(s3): gate CE quota OSS stubs to not(enterprise) to match callers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(s3): delta-aware CE storage quota + guard usage-load retry loop
Account for the overwritten object's size in the quota check so valid
same-size overwrites near quota are not rejected (Codex review), and stop the
storage-usage $effect from re-firing on persistent API errors (Pi review).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(s3): count chunked PUTs; revert overreaching volume quota copy
Volumes write to workspace storage via a separate worker-side path with its own
50MB-per-file cap that this PR does not change, so revert the drawer copy that
claimed they count toward the 10GiB quota (Codex review). Bump ee-repo-ref for
the chunked-PUT accounting fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(s3): add SQLx cache for CE usage-bump/quota queries; exclude volumes
Regenerate the missing offline SQLx cache for the not(enterprise) bump and
remaining-quota queries so the private CE offline build compiles, and bump
ee-repo-ref for the volumes/-prefix exclusion from the counted quota (Codex
review).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(s3): always HEAD for CE upload delta so overwrites don't inflate usage
Bump ee-repo-ref for the fast-path overwrite-accounting fix (Codex review).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(s3): reserve volumes/ prefix on CE write surfaces to close quota bypass
Reject direct writes to the reserved volume prefix on the app-upload surface and
add the OSS stub; bump ee-repo-ref (Codex review).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(s3): refuse new multipart work when CE workspace is at quota
Bump ee-repo-ref for the multipart-initiate/part quota gate (Codex review).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(s3): reserve in-flight multipart bytes against CE storage quota
Add workspace_multipart_inflight table + grants, SQLx cache for the reservation
queries, and bump ee-repo-ref. Bounds abandoned multipart uploads that the
list-based recount can't see (Codex review).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(s3): clear multipart reservation only after a successful complete
Add exclude-upload arg to the OSS quota stub/caller and the SQLx cache for the
updated remaining-quota query; bump ee-repo-ref (Codex review).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(s3): per-part multipart reservation; commit only on part success
Per-part workspace_multipart_inflight schema (upload_id, part_id) so retries
replace rather than double-count; SQLx cache for the reworked queries; bump
ee-repo-ref (Codex review).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* perf(s3): HEAD the multipart overwrite target once per upload, not per part
SQLx cache for the stored-credit lookup; bump ee-repo-ref.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: update ee-repo-ref to bea5a8b5120d6d69cab1ad4611ebe463559bd200
This commit updates the EE repository reference after PR #640 was merged in windmill-ee-private.
Previous ee-repo-ref: 6e6ff86f1939cf74736b7d435bf6851416437523
New ee-repo-ref: bea5a8b5120d6d69cab1ad4611ebe463559bd200
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat: support workspace forks on cloud using parent workspace limits
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: clarify count_paid_seats approximates rather than mirrors billing seats
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: non-admin fork UI, attach cap, and fork-count for cloud forks
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: cloud fork billing cache on rename, usage display, attach cap edge
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: fork count in cloud quotas + fork billing points to parent
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: invalidate billing/fork caches on fork deletion for id reuse
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: gate fork usage remap on CLOUD_HOSTED, not just the cloud feature
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: note cloud feature vs CLOUD_HOSTED gating in backend guide
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: reserve fork-cap slots for an attach candidate's whole subtree
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: invalidate team-plan cache on delete, raise fork depth cap
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: cap fork nesting depth (MAX_FORK_DEPTH, default 5)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: fork count/height robust to cycles and deleted intermediates
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): reset fork button loading state on creation error
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: invalidate billing cache for attached fork subtree; helper auth docs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: invalidate bun bundle cache on transitive relative-import changes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: do not memoize transient fetch errors in bundle-key import cache
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: use regular comment on lazy_static block (deny unused_doc_comments)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: align bundle-key import version selection with loader content endpoint
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): show inline workspace name editor on general settings
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): guard rename and support enter-to-save on workspace name
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai): add Azure AI Foundry as a native AI provider
Adds `azure_foundry` as a new AIProvider variant wired through the AI
chat (copilot) and AI agent flow steps. Foundry's chat completions API
is OpenAI-compatible and uses Azure conventions (api-key header, Azure
URL building), so it reuses the existing OpenAI-compatible query builder
and proxy path via the shared `is_azure` helper (renamed from
`is_azure_openai`).
Backend (windmill-ai):
- New `AzureFoundry` enum variant (serde `azure_foundry`)
- `get_base_url` requires a resource base URL (like Azure OpenAI / Custom)
- `is_azure()` covers Azure OpenAI + Foundry (api-key auth, Azure URL)
- Added to OpenAI-compatible proxy support and HttpForward proxy mode
- New proxy URL unit test
Frontend (copilot):
- New provider entry, completion config, model-token handling, streamed
usage tracking, and reasoning registry (all model-id-gated, so a no-op
for Foundry's non-OpenAI catalog)
- Treated as a chat-completions provider, not the OpenAI Responses API
OpenAPI:
- `azure_foundry` added to AIProvider (openapi.yaml) and AIProviderKind
(openflow.openapi.yaml); regenerated CLI guidance
Note: the `azure_foundry` resource type (base_url + optional api_key) is
hub-managed and must be published to the Windmill Hub separately.
Fixes WIN-2122
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai): add azure_foundry to copilot flow Zod provider enum
The tracked copilot flow schema (openFlowZod.gen.ts and its openFlow.json
source) still carried the old AIProvider enum, so validateFlowModules /
validateSpecialFlowModule rejected AI-generated flow edits that create or
update an aiagent module with provider kind "azure_foundry" before they
could be saved. Add the value to both (preserving the generated single-line
format) and a regression test over the flow-module validation path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai): lead provider list with OpenAI, Anthropic, Google AI
Reorder AI_PROVIDERS so the three primary direct providers come first. The
AIProviderPicker renders the first three entries as quick-access buttons, so
these become the defaults (previously OpenAI, Azure OpenAI, Azure Foundry);
Azure OpenAI / Azure Foundry stay adjacent right after. No logic depends on
provider order (only per-provider defaultModels[0] is read).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): home New submenus fall back below hugging the right edge
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): re-hug submenu on window resize even without a melt reposition
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix: revalidate workspace items cache on context picker open
The chat context picker (and workspace drill pickers) read from a
module-level cache that was only invalidated from two editor save paths,
so items created or deleted anywhere else stayed stale until a full page
reload. Make the loader do real stale-while-revalidate: cached items
render instantly and each workspace+kind is re-fetched once per picker
mount, keeping the state reference stable when nothing changed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: retry failed picker revalidation and handle its rejection
Mark a workspace+kind as revalidated only after the fetch succeeds, so a
failed revalidation is retried on the next ensureLoaded call instead of
stranding stale data for the rest of the mount. Catch the rejection
(callers fire-and-forget) and log it. Also dedupe the
stale-while-revalidate rationale to its canonical comment site in
workspacePicker.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
A websocket trigger is permanently disabled (with a critical alert) when a
single connect attempt in get_consumer fails. Gateway endpoints fronted by
edge proxies (e.g. Discord behind Cloudflare) sporadically answer the
upgrade handshake with a transient 502/520, so a long-lived trigger that
reconnects frequently eventually catches one and dies until a human
re-enables it.
Retry transient failures (HTTP 5xx/429 handshake responses and IO errors)
up to 5 attempts with exponential backoff before surfacing the error, and
report retry progress through the trigger's error status. Permanent-looking
errors (bad URL, other 4xx, protocol/TLS mismatch) still disable immediately.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(pipeline): backfill a range of partitions from the asset drawer (ee)
* feat(pipeline): cancel in-flight backfill job and show cancelling state
* refactor(pipeline): move backfill range logic behind private feature
* fix(pipeline): close backfill cancel-launch race and record dispatch intent
* docs(openapi): producer_path also covers SDK write-edge producers
* chore: update ee-repo-ref to c3852ecb36bd0be1a74c63169e513888f3347850
This commit updates the EE repository reference after PR #641 was merged in windmill-ee-private.
Previous ee-repo-ref: 7c1450ef89fbc9e844a121b39cafe0d7235d704b
New ee-repo-ref: c3852ecb36bd0be1a74c63169e513888f3347850
Automated by sync-ee-ref workflow.
---------
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix: scope SCD2 built-in data tests to current rows
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: add --partition to pipeline run and fix duckdb s3object upload binding
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: note filesystem storage type is dev-only in storage settings
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: use ISO week for weekly partition default in pipeline run
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): reserve fixed height for unsaved-changes banner to avoid content shift
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): remove border around reserved banner slot and shrink it
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): tighten top padding under the unsaved-changes banner
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): give unsaved-changes banner buttons minimal vertical breathing room
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(frontend): drop redundant Metadata section title in trigger and script editors
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): tuck schedule editor labels under summary to match convention
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* revert(frontend): keep Metadata section title in ScriptBuilder for a separate PR
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): drop leftover header-content margin on headless Section
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): drop top padding above resource editor first field
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(frontend): match variable editor bottom padding to resource
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): add Path label in new resource form to match edit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): reserve half the banner height to halve the idle gap
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): reserve a third of the banner height when idle
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): don't reserve banner slot or tighten top for new entities
Gate the reserved-height slot and the tight content top padding on the banner's baseline (bannerReserved) instead of merely on the banner snippet being present, so new-entity drawers keep normal top spacing and add no empty slot.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(frontend): trim banner comments to the 4-line invariant limit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(frontend): describe partial-reserve banner behavior accurately
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): keep pipeline graph layered when lineage has cycles
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): fit pipeline graph to visible canvas on initial load
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): style pipeline minimap so it reads as a minimap
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(parser): don't infer s3 reads from bare string-literal mentions in sql
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(duckdb): render temporal values as ISO strings in job results
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): key pipeline viewport fit on the loaded graph's folder
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(parser): treat list/named read-fn arguments as definitive s3 reads
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(frontend): surface pipelines in sidebar nav, index page and sql editor hint
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(frontend): remove pipelines sidebar nav item
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(frontend): group compare & deploy items by folder
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): group folder items with their folder, fix disabled hint
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(duckdb): auto-declare the partition arg for // partitioned scripts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(cli): pipeline run --arg to pass plain run args to cascade scripts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The cron schedule row placed the input next to a shrink-0 button group, so
on narrow layouts the buttons kept their width and squeezed the input to
near-zero. Make the row wrap and give the input a min width so the buttons
drop below it, keeping both visible.
Fixes WIN-2121
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fetching options for a `dynselect`/`dynmultiselect` input was inconsistent
between deployed scripts and deployed flows:
- scripts ran through `push_script_job_by_path_into_queue` — a `script` job
with the script's path, tag, lock and codebase resolution;
- flows ran their schema dyn-select code as an anonymous `preview` with no
path and no tag (always the language default), and reported access
failures as a raw `SqlErr: no rows`.
Deployed scripts are left exactly as they were (that path already handles
tag/lock/codebase/on-behalf-of correctly). The flow branch now:
- carries the flow path on the preview job,
- reads the flow's `tag` under RLS and routes the job to it (falling back to
the language default when unset), matching the script's worker group, and
- runs `check_tag_available_for_workspace` on that tag — the same gate a
normal flow run and the script path apply — so a caller who can read the
flow but is not allowed to use its (custom/scoped) worker tag is rejected
consistently.
The flow's tag read runs on every request, so it also serves as the
per-request access check, replacing the raw error with a clean
`NotAuthorized` / `NotFound`. Entrypoint-name validation now covers all
branches (it is interpolated into the generated wrapper). Inline is
unchanged: a `preview` with no path on the language default, blocked for
operators.
Fixes WIN-2118
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(forks): let partial-visibility users deploy the visible subset
The fork Compare & Deploy page hid the deploy button entirely whenever the
comparison reported any item not visible to the user (all_ahead/all_behind
flags), telling them to hand the deploy to someone with full access. But the
non-visible items are already filtered out of the diff list, and the UI already
supports deploying an arbitrary subset via per-item selection — so blocking
everything was inconsistent and, for stale/phantom rows, blocked on items that
don't even exist.
Show the deploy footer regardless; the user acts on the visible/selected items
(the per-item disabled conditions are unchanged). The hidden-items notice is
kept but downgraded to a non-blocking, direction-scoped banner that explains the
excluded items instead of removing the action.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(forks): surface hidden-item counts by kind + admin path list
WIP: expose items dropped by the visibility filter (hidden_ahead/hidden_behind
in the compare response): by-kind counts for everyone, kind+path only for admins.
* fix(forks): don't close the deployment request on a partial (hidden-items) deploy
Making the deploy button reachable in the partial-visibility case exposed a bug:
a clean merge-into-parent deploy unconditionally closed any open fork deployment
request as "merged" — marking its comments obsolete and notifying the requester
and assignees of a merge — even when hidden ahead changes were excluded from the
list and left undeployed. Only close the request as merged when the full ahead
set was visible (all_ahead_items_visible); otherwise leave it open (with a toast)
so someone with full access can finish it.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): correct misleading delete-fork command description
The `wmill workspace delete-fork` description claimed it deletes "a
forked workspace and git branch", but the implementation only deletes
the Windmill workspace via the backend API and removes the local
workspace profile. No git operations are performed, so the remote
branch is left untouched. Drop the "and git branch" clause and
regenerate the derived guidance/system-prompt files.
Fixes WIN-2120
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(cli): permanently delete temp workspaces in folder test cleanup
The isolated-workspace test helper archived each temp workspace on
teardown. After #9865 added a CE cap of 1 archived workspace, the second
archive-cleanup is refused, so temp workspaces leak into the active set
and hit the 2-workspace CE cap — failing every subsequent create/fork
across the shared test backend.
Permanently delete the workspace instead (DELETE /api/workspaces/delete),
which frees the slot without occupying the archived quota.
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: Ruben Fiszel <ruben@windmill.dev>
The blast-radius guard added in #9866 forced `all_ahead_items_visible` true for
any fork/target admin. But `filter_visible_diffs` keeps a modified/conflict row
(one that exists in the source AND the fork) only when the caller can see it on
both sides, so an ahead diff can be dropped for a source-side visibility gap even
when the caller is a fork admin. Forcing the flag on fork-admin alone then wrongly
reported "all ahead items visible", letting the UI enable deployment from an
incomplete comparison.
Gate the guard on admin of BOTH the source and the fork (superadmin satisfies
both), which is what actually guarantees full visibility of every item on every
side. Adds a regression test where a fork admin who is only a plain member of the
parent (no access to the item's folder) must still get `all_ahead_items_visible
= false`, plus the superadmin sanity path.
Also restores the SQLx offline cache entry for the phantom-trigger test INSERT
that #9866 landed without (CI/`SQLX_OFFLINE=true` builds failed on it), and adds
entries for the new test's all-literal queries.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deleting a trigger left a stale `workspace_diff` row: `delete_trigger` (the
generic TriggerCrud handler) was the only delete path that never called
`handle_deployment_metadata`, unlike every other kind. Because
`compare_workspaces` trusts a cached `has_changes=true` row for non-script/flow
kinds and the visibility filter then drops it (the trigger no longer exists), a
deleted trigger became a phantom "ahead" item that flipped
`all_ahead_items_visible` to false — hiding the deploy button and showing a
"changes not visible to your user" warning that even a superadmin could not
resolve (`reset_diff_tally` doesn't clear a `has_changes=true` row either).
- delete_trigger now re-tallies via handle_deployment_metadata, so the next
compare re-evaluates and corrects/removes the row (matches resource/variable/
folder/schedule deletes).
- compare_workspaces forces the visibility flags true per side for anyone who
sees that side in full: target/fork admin (or superadmin) for ahead items,
source/parent admin (or superadmin) for behind items. The flag is a pure
visibility guarantee — the deploy itself is authorized separately — so for
such users a dropped diff is provably a phantom, never a permission gap.
- Add a regression test asserting a phantom trigger diff row no longer blocks a
superadmin while still (conservatively) warning a partial-context user.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(workspaces): enforce CE workspace limit when unarchiving
Unarchiving a workspace re-activates a soft-deleted (deleted = true)
workspace, effectively bringing it back to the active set. On CE this
bypassed the 2-workspace cap that create_workspace enforces, letting a
user exceed the limit by archiving and re-unarchiving.
Run the same _check_nb_of_workspaces guard before flipping deleted back
to false. The workspace being restored is still deleted = true at that
point, so it is correctly excluded from the count.
Fixes WIN-2119
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(workspaces): cap CE archived workspaces at 1
Complements the unarchive-limit fix: without a cap on archived
workspaces, a CE user could stockpile many soft-deleted workspaces (each
of which still occupies its workspace id and can later be unarchived).
Refuse a new archive on CE when an archived workspace already exists,
mirroring the create/unarchive workspace-count guards.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: use derived username instead of email for non-member superadmins
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: address review - drop redundant username cache, guard whoami membership by email
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor: use explicit non_member boolean instead of role string for superadmin banner
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: resolve email from password table for non-member superadmin permissioned_as
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: resolve non-member superadmin drafts via shared username->email resolver
Adds resolve_username_to_email (usr, then super_admin password fallback for both derived-username and email modes) and uses it in get_email_from_permissioned_as and the drafts get/list endpoints, so a non-member superadmin's drafts resolve and no email leaks into the drafts payload.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: superadmin-not-in-workspace schedule uses derived username as permissioned_as
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: resolve non-member superadmin identity in draft owner-circles, username_to_email, and home filter
Applies the password-fallback username resolution to the script/flow/app/draft owner-circle subqueries and the username_to_email endpoint (was an admins-workspace 'username == email' hack), and switches the home items-list user-folder filter to the non_member flag instead of the now-broken username-contains-@ heuristic.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: backfill non-member superadmin favorites from email to derived username
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: propagate DB errors in username resolution instead of leaking email (CI review)
Addresses cubic-dev-ai P2: get_instance_username_or_fallback_to_email now returns Result and only falls back to the email for a genuine 'no derived username'; a query error propagates so callers fail closed rather than leaking the raw email as the acting username.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: clarify non-member superadmin popover (username used + admin permissions)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: keep username_to_email endpoint member-only to not disclose non-member superadmin email (CI review)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: forbid disabling automate_username_creation once usernames assigned (CI review)
Makes the setting effectively one-way once instance-wide usernames exist, so the global-uniqueness invariant that keeps stored u/<username> identities (schedules/triggers/drafts/superadmin ownership) unambiguous can never be dropped back to workspace-local uniqueness. Re-saving false on an already-disabled instance stays a no-op.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(offboarding): make global reassignment per-workspace and optional
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(offboarding): handle sole-member workspaces in workspace-level removal
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(offboarding): show close action instead of dead-end in reassign-only sole-member case
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(offboarding): prevent no-op success in global reassign-only with no reassignable workspace
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): add zoom and download to Mermaid graphs
Mermaid diagrams in the AI chat could only be viewed inline with
horizontal scroll. Add a download-as-SVG button and an expand button
that opens a fullscreen modal with pan/drag and zoom (mouse wheel plus
in/out/reset controls), reusing the existing `panzoom` dependency.
Fixes WIN-2117
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): guard panzoom action against close-before-import race
Address review nit: if the modal closes before the dynamic
import('panzoom') resolves, destroy() ran while instance was still
undefined (disposing nothing) and the late .then() built a leaked
panzoom on a detached node. A disposed flag makes the cleanup airtight.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Folder owners use the format u/<username>, and usernames are frequently
email addresses containing `.` and `@`. The folder creation path bypasses
validate_owner(), so these owners get inserted successfully, but add_owner
and remove_owner both call validate_owner() and rejected any later
modification of email-style owners.
Extend the character allowlist to accept `.` and `@` (and update the error
message). SQL injection risk is already mitigated by the bind-parameter
queries introduced alongside this validation.
Fixes WIN-2116
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(pipeline): local development for data pipelines (CLI --local + pipeline dev preview)
Add the local edit→preview→run loop for data pipelines (folders of `// pipeline`
scripts), the analog of `wmill dev` / `wmill app dev`, usable from a code editor
or an agentic loop — without deploying.
No backend changes: full body inference comes from the same wasm the frontend
uses (windmill-parser-wasm-asset), which returns assets + pipeline annotations in
one call; local runs reuse runScriptPreview with _wmill_skip_asset_dispatch.
- localGraph.ts: wasm-backed working-tree → asset-graph builder (the enabler)
- pipeline show/run --local; new pipeline docs (PIPELINE.md/AGENTS.md) subcommand
- pipeline dev watcher + /pipeline_dev page (PipelineDevView) rendering the same
PipelineGraphEditor from the pushed local graph, run via preview
- cascadeRun.ts: reusable run primitives extracted from the route page
- regenerated CLI agent docs
See docs/pipeline-local-dev.md for the full design, test steps, and handoff TODOs.
The live `pipeline dev` browser preview is implemented but not yet stack-verified.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(pipeline): improve local dev preview (run, activity, responsive)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(pipeline): dev-preview args, multi-root run, ws auto-reconnect
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): connect managed-materialize producer in local dev graph
The CLI pinned windmill-parser-wasm-asset ^1.728.1, which predates managed-materialize support (added in 1.733.1); the frontend already pins 1.740.0. The CLI's wasm therefore never emitted `// materialize`, so the producer had no output edge and showed disconnected from its `// on` consumers. Bump the CLI to 1.740.0 (matching the frontend) and translate the parsed materialize target into the producer's write edge + materialize_target, mirroring frontend resolveGraph.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): harden local-dev CLI (bare-.sql crash, defaultTs, docs clobber)
Review fixes, complementary to the dev-preview/materialize/multi-root work already
on the branch (none overlap those commits):
- localGraph: a bare `.sql` (no dialect) made inferContentTypeFromFilePath throw and
abort the whole graph build — and wedge `pipeline dev` at startup. Skip the
unclassifiable file instead. Also map `bunnative` → parse_assets_ts and add
ruby/rlang/nu/powershell to the `#`-comment fallback.
- show/run/docs/dev: thread the resolved `wmill.yaml` defaultTs into the graph
builder so `.ts` infers under the workspace's runtime (bun vs deno) instead of
always bun — `opts.defaultTs` was always undefined (no such CLI flag).
- dev: wrap the startup graph build so a half-written file can't abort the watcher.
- docs: don't clobber a user-authored AGENTS.md/CLAUDE.md — only (over)write the
pointer when absent or already a generated `@PIPELINE.md` pointer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): bind dev WS to loopback + local-graph regression tests
- pipeline dev WS broadcast the folder's full script source (scripts[].content + temp_script_refs) unauthenticated on 0.0.0.0:3201 — bind 127.0.0.1 so it's not LAN-reachable (webview localhost + SSH/devbox port-forward still work).
- Add regression tests for the just-landed local-graph fixes: bare .sql is skipped (was a build/dev-startup crash), defaultTs threads into .ts runtime inference (bun vs deno), and #-comment languages (ruby) use the # annotation fallback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(pipeline): --frontend flag for pipeline dev page origin
wmill pipeline dev opens <remote>/pipeline_dev, but that route only exists in this build's frontend, so it 404s against a remote whose deployed frontend predates it. --frontend <origin> points the page at a locally-run frontend (REMOTE=<remote> npm run dev) while the API/token still target the remote — enabling the live preview against a real backend before the PR is deployed. No behavior change when omitted. Regenerated CLI agent docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): WS session token + details-pane live-reload refresh
Addresses CI review (Codex/Pi/Claude):
- dev WS: a browser tab could open ws://localhost:<port>/ws and receive the folder's full source (browsers don't enforce same-origin on WS, loopback bind alone doesn't help). Gate the upgrade on an unguessable per-session token carried in the dev-page URL (verifyClient → 401 without it). Verified: no-token/bad-token connections get 401 with no bundle.
- details pane: scriptRes keyed on [workspace, selection, draftScript] didn't re-run on a pipeline dev live-reload (same selection), so the open pane showed stale source. Thread a localScriptsVersion (the pushed bundle) into the key. Verified: editing a selected node's file updates the pane source without reselect.
- docs/pipeline-local-dev.md: refresh the stale 'not yet exercised' status + done TODOs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): emit volume: annotation assets in local dev graph
Addresses CI review (Codex P1 / Pi P1): the wasm body parser doesn't surface `// volume: <name>` annotations — the frontend (infer.ts:parseVolumeAnnotations) and backend (asset_inference.rs) parse them separately and merge as rw volume assets. localGraph didn't, so a `# volume: cache` producer had no write edge and showed disconnected from its `// on volume://cache` consumer (and pipeline run --local wouldn't schedule downstream). Mirror the leading-comment-block scan (SQL excluded, matching both reference parsers) and merge into inferScriptAssets. Regression test added; verified producer -> volume://cache -> consumer connects.
Also (Codex P2): docs/pipeline-local-dev.md manual browser URL omitted the new ws_token param — without it the WS upgrade is rejected and the page sits disconnected. Doc now says to copy the URL the CLI prints (carries wm_token + ws_token) and recommends --frontend.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): runAll excludes event roots + review polish
Addresses CI review (Codex P1, Claude P2/P3):
- pipeline run runAll: derive the whole-pipeline selection from validStarts + descendants instead of all runnables, so an unqualified 'pipeline run <folder>' no longer fires event-trigger roots (kafka/mqtt/…) with empty args/side effects. Verified: a kafka root is excluded from the plan.
- cascadeRun.ts runBoundedCascade: use buildLineageDownstreamMap (read-aware) so a pure-reader runs after its producer, and return cyclic — parity with the route page's bounded run (the file is meant to be THE shared correct primitive).
- PipelineGraphEditor: storedRightPaneSize starts at 0 so the orientation-aware default (55% stacked / 40% side-by-side) actually applies on first open.
- localGraph fallbackParse (go/bash): scan only the leading comment header (no body-comment phantom triggers) and strip key=value options from the asset URI; regression test added.
- docs: reject '..' in the folder arg (it writes files under f/<folder>).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): route local previews to the // tag worker
Addresses CI review P1: the local graph/bundle dropped the parsed `// tag`, so a node annotated `// tag gpu` ran on the default worker in both `pipeline run --local` and `/pipeline_dev`, while the deployed pipeline routes it to that worker tag. Carry the tag through LocalScript / the pushed bundle / LocalScriptContent and pass it to runScriptPreview at all three launch sites. Verified: a duckdb node tagged `bash` produces a job tagged `bash`; regression test added.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): add asset partitions/schemas routes to OpenAPI, use generated client
The ducklake asset panels (PartitionStatusGrid, SchemaHistoryPanel) hit /assets/partitions and /assets/asset_schemas via raw fetch with cookie-only auth, because those backend routes were never added to openapi.yaml so the generated client had no methods for them. On /pipeline_dev (token-via-URL, no session cookie) the raw fetches 401'd. Add both GET routes + MaterializedPartition/AssetSchemaVersion schemas to openapi.yaml and call them through AssetService, which injects the bearer token, types, and cancellation automatically. Verified: Partitions + Schema tabs load in /pipeline_dev. (backfill stays a raw fetch — it's an EE-only route not in the OSS spec — with the token added inline.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(cli): regenerate bun.lock for windmill-parser-wasm-asset
package.json / package-lock.json carry windmill-parser-wasm-asset@1.740.0 but the tracked bun.lock (the CLI installs/builds/tests via bun) was stale, so fresh bun installs would resolve a different graph than the committed lock. Regenerated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): show asset producer + its runs in the dev-preview panel
Selecting a ducklake/asset node in /pipeline_dev showed 'No producer for this asset' because selectionProducers wasn't passed (it's derived from the deployed graph on the route page, absent here). Compute it from the local graph's w/rw write-edges (incl. the // materialize target) and pass it through, mirroring the route page — so the panel shows the producing script and its (preview) runs, including data-test failures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): carry annotation metadata onto local-graph runnables
The local graph emitted only path/usage_kind/in_pipeline/materialize_target per runnable, so /pipeline_dev and pipeline show --local weren't the same surface as the deployed graph for annotated scripts — missing the badges/lineage the shared canvas renders. Map the wasm-parsed partition_kind, freshness, tag, retry, data_tests, column_lineage, and materialize_strategy (derived append/merge/replace) onto each runnable, mirroring the deployed AssetGraphRunnableNode.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): exclude event handlers that are lineage descendants from runAll
The runAll guarantee ('never fires an event handler with empty args') only held for event ROOTS — validStarts excludes them, but runAll then unions in descendants(dag, start), so a kafka/mqtt/... handler that also reads an upstream pipeline asset (a lineage descendant of a valid start) still landed in the plan. Add eventTriggerScripts() and subtract it from the selection after the descendant union. +unit test.
Also: docs/pipeline-local-dev.md recipe used 'pipeline docs demo_pipeline' without --local (default queries the deployed graph → hits the empty hint); add --local.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): whole-pipeline run cuts at event handlers (drop their downstream too)
The prior runAll fix subtracted event handlers from the selection but left their downstream: for manual_root → asset_x → kafka_handler → asset_y → consumer, deleting only kafka_handler left consumer selected, and topoOrder then ran it as a root with missing/stale event-derived inputs. Replace the descendant-union+delete with reachableCutting(dag, validStarts, eventHandlers): traverse from valid starts but treat event handlers as cut points, so a node reachable ONLY through an event handler is dropped while one reachable via a non-event path stays. +unit test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): recover // tag in the go/bash annotation fallback
The wasm path carries out.tag, but the go/bash fallback (and the wasm-error degradation path) only recovered pipeline + on, so a // tag gpu on a bash/go node — or a temporarily-unparseable ts/py/sql node — silently routed the local preview to the default worker while the deployed pipeline routes to the tag. Scan for // tag in fallbackParse too. +test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(pipeline): extract shared assetProducers helper
The 'who writes this asset' write-edge derivation was copied verbatim in PipelineDevView and the pipeline route page — two copies that would drift. Extract assetProducers(graph, selection) into graphTraversal.ts and use it from both, keeping the dev view and route page in lockstep.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): only overwrite AGENTS.md/CLAUDE.md when it's the exact generated pointer
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): wire local-dev runs into the selected-node runs pane
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): exclude data_upload/webhook entrypoints from auto CLI runs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(pipeline): --upload binds an object to a data_upload/webhook entry point
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(pipeline): add "Run + downstream" to the dev preview detail form
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): cut non-autorun triggers on all run paths; multi-binding --upload
Address CI review: apply the data_upload/webhook/event barrier cut to the
single-root and bounded (--from/--to) paths, not just whole-pipeline; accumulate
repeatable --upload bindings per script (were overwritten); scope dev upload keys
by script+param to avoid basename clobbering; drop <script> from help text.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): reseed dev run form when a local edit changes the script's args
The read-only pane is keyed on script.path only, so in /pipeline_dev the selected
node re-resolves on every WS bundle without remounting; PipelineScriptView cloned
script.schema once, so adding/removing args left the run form on a stale schema
(could run with missing inputs). Extract PipelineRunForm (owns the SchemaForm
clone) and key it on the serialized schema: a real arg change reseeds the form,
an unchanged re-resolve keeps in-progress input.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): don't cut a scheduled/manual root that also has a non-autorun trigger
Address Codex P1: the barrier set subtracted only --upload-bound scripts, so a
script with both `// on schedule` and `// on data_upload` resolved as the start
yet was also a barrier — reachableCutting skipped it, giving an empty run plan.
Subtract all valid starts (schedule/manual roots + bound handlers) from barriers:
a legitimately-scheduled root runs on its schedule path even if it also carries a
caller-input trigger; pure input-only roots stay cut. Adds a regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): deployed non-autorun enrichment, s3:// storage, --to cut accounting, tag regex
Address CI review (Codex P1/P1/P2, Pi P2):
- Deployed `pipeline run` recovers marker-only data_upload/webhook/email triggers
from script bodies (like the `show` path) so input-only entrypoints are cut
instead of auto-run empty on the deployed graph.
- `--upload s3://<storage>/<key>` keeps the named storage (authority) instead of
folding it into the key, matching the S3Object round-trip convention.
- Bounded `--to` targets cut by a barrier are reported in droppedEnds (+warning),
not reachableEnds.
- fallbackParse `// tag` matches a single token (\S+), rejecting multi-word prose.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): header-only deployed marker scan, fail-closed enrichment, default-storage s3 keys
Address CI review (Codex P2, cubic P1/P1/P2):
- Deployed marker recovery scans the LEADING comment header only (shared
recoverHeaderMarkers helper, reused by the show enrichment too) so a body
comment `// on data_upload` can't inject a phantom trigger and over-cut.
- Deployed run enrichment fails CLOSED: a script-body fetch error aborts the run
instead of silently letting an input-only entrypoint run with empty args.
- Revert `--upload s3://` to default-storage whole-path keys (matching pipeline
`s3://` asset-URI semantics); named-storage authority-splitting broke nested
default keys like `s3://raw/2026/events.csv`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): reject trailing content on fallback native markers; trim s3:/// key
Address CI review (Codex P2, cubic P3):
- fallbackParse now requires a native marker (`// on data_upload`) to stand alone;
a line with trailing content (`// on data_upload f/foo`, `# on kafka topic`) is
rejected, matching the canonical parser and keeping local/deployed parity.
- s3UriKey trims a leading slash so the canonical empty-authority default form
`s3:///key` doesn't leak a leading slash into the object key.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): persist dev WS token per-port so reconnect survives a CLI restart
Address Codex P2: the /pipeline_dev auto-reconnect reuses the ws_token from the
page URL, but `pipeline dev` minted a fresh random token each start, so a restart
on the same port left the open page rejected by verifyClient forever. Persist the
token per-port under the user-private config dir (0600) and reuse it on restart,
so an already-open page reconnects — matching the reconnect behavior's intent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): scope persisted dev WS token by workspace+folder+port
Address cubic P2: keying the persisted token by port alone let a stale browser
tab from a previous folder's session on the same port reconnect and receive a
different folder's source. Scope the token file by workspace+folder+port so a
same-session restart still reconnects, but a different folder on the same port
gets a distinct token that rejects stale cross-folder tabs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): caller args can't override skip-dispatch guard; hash the dev token key
Address CI review (Codex P1, cubic P2):
- makeLaunch / CLI run build args with `_wmill_skip_asset_dispatch` LAST (and drop
any caller-supplied copy) so a run-form/`--upload` arg can't re-enable backend
asset dispatch while the client orchestrates the cascade (double-run / running
deployed subscribers from a local preview). Adds a cascadeRun guard test.
- Dev WS token file key is a sha256 of NUL-delimited workspace+folder+port, so
different folders (`a/b` vs `a_b`) can't collide onto the same token file.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): canonical s3://storage/key --upload parsing; scope dev token by remote+root
Address Codex P1/P1:
- Restore canonical S3Object URI parsing for `--upload` s3 sources, matching the
frontend's `parseS3Object` (`s3://<storage>/<key>`, empty authority ⇒ default,
`s3:///key`/`s3:///nested/key` for the default store). `s3://secondary/k.csv` →
`{ s3: "k.csv", storage: "secondary" }` so a named-storage object is read from
the right store. (This is the canonical convention; the default-storage nested
key is served by the `s3:///` form.)
- Scope the persisted dev WS token by remote+workspace+root+folder+port (was
workspace+folder+port), so two profiles on different remotes (or local checkouts)
with the same workspace/folder/port don't share a token — a stale tab can't
reconnect across a workspace/remote boundary.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MCP tool arguments were converted to URL query values via `value.to_string()`
+ `trim_matches('"')`. For string values containing JSON (e.g. the `args`/`result`
filters on job listing, `args` on schedule listing), `to_string()` JSON-encodes the
string and escapes inner quotes with backslashes; stripping the outer quotes leaves
`{\"k\":\"v\"}`, which the backend's `serde_json::from_str` then fails to parse,
falling back to `FALSE` and returning zero results.
Use `value.as_str()` to emit the raw string content for `Value::String`, falling
back to `value.to_string()` for non-string types (numbers, booleans). Adds
regression tests covering JSON-string, non-string, and plain-string params.
Fixes WIN-2114
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Same grant gap already fixed for notify_event (20260619091631),
script_trigger (20260619112847), and dispatch_event: tables created after the
one-time GRANT ALL in 20250205131523 rely on ALTER DEFAULT PRIVILEGES, which
only covers objects created by the role that set them. On deployments whose
migration runner is a different role, these tables end up ungranted, and writes
that run under the RLS role (a transaction opened via user_db.begin(&authed) ->
SET LOCAL ROLE windmill_user/windmill_admin) fail with "permission denied for
table <name>".
Audited every table created after 20250205131523: these three are the only
ones with a confirmed write on a user_db transaction that lacked a grant:
- workspace_diff: UPDATE in set_ws_specific (workspaces.rs)
- materialized_partition: INSERT via record_materialization (assets API);
sibling materialized_asset_schema was already granted
- debounce_stale_data: DELETE in resume_suspended_trigger_jobs (global_handler.rs)
GRANT is idempotent so re-application is a no-op.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(pipelines): add managed SCD2 history materialize strategy
`// materialize ducklake://... key=<col> history [track=...]` (alias: `scd2`)
upgrades the keyed merge to SCD type 2: diff the current snapshot against live
rows, close changed versions (valid_to/is_current) and open new ones in one
transaction, keeping full history. Adds a consumer-convenience <dim>_current
view; effective-dated joins via native ASOF JOIN >= valid_from. Managed, so
// data_test and schema capture work (unlike manual mode). Non-partitioned v1,
soft-delete on absence.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(pipelines): document scd2 track= spacing, reserved _current suffix, schema-freeze
Addresses non-blocking CI-review nits on the new SCD2 public surface.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipelines): null-safe scd2 key matching + create _current view inside txn
Addresses CI review: (1) Codex P1 — NULL natural keys were flagged as changed
but silently dropped because `key IN (...)` never matches NULL; close/open now
match with `IS NOT DISTINCT FROM` via correlated EXISTS. (2) cubic P2 — the
`<dim>_current` view was created after COMMIT and CREATE VIEW advances the
DuckLake snapshot, so the summary recorded the view's snapshot instead of the
data write; the view is now created inside the write transaction. Validated both
against a real DuckLake (NULL key materialized; one snapshot per run).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipelines): create scd2 _current view with IF NOT EXISTS to keep no-change runs no-op
Addresses CI review (Codex P2): CREATE OR REPLACE VIEW advances the DuckLake
snapshot every run, so an unchanged rerun still minted/recorded a snapshot. The
view definition is static, so IF NOT EXISTS creates it once (folded into the
first data-write snapshot) and is a true no-op thereafter — verified an unchanged
rerun keeps max(snapshot_id) constant. Also softens the reserved-name collision:
IF NOT EXISTS skips silently instead of erroring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(pipelines): add scd2 deletes=close (hard-delete-close)
Opt-in `deletes=close` closes the current version of a key that disappears from
the snapshot (dbt's hard_deletes=close); default stays soft-delete. Codegen adds
a vanished-key temp set (current keys EXCEPT snapshot keys) + a second null-safe
close UPDATE with no reopen; a reappearing key opens a fresh version (validity
gap = correct SCD2). Wired through both parsers with parity fixtures/tests, worker
derivation, unit + codegen tests, and docs. Verified end-to-end against a real
DuckLake incl. delete-close + reactivation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipelines): align materialize deploy precedence warning with runtime (scd2>append>merge)
The deploy-time conflict warning only knew append>key, so
warned 'append wins' while the runtime (duckdb_executor) runs SCD2 (history wins).
Warn for history+append (history wins, append ignored) before the append+key case,
mirroring the runtime strategy precedence. (Pi review P2.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipelines): register scd2 _current view as a produced asset for cascade dispatch
The docs present the companion <dim>_current view as a subscribable produced asset
(// on ducklake://.../<dim>_current), but deploy registered only the base table as
a write asset, so a subscriber on the view would never be dispatched (the cascade
fans out from deploy-time asset rows). Register <dim>_current as a produced write
asset when scd2 so those subscribers fire. (Codex review P1.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipelines): don't register _current asset for manual+history (no view created)
Manual mode short-circuits before the scd2 codegen, so no <dim>_current view is
created; gate the produced-asset registration on !manual so a contradictory
// materialize manual ... history doesn't leave a false write edge dispatching
subscribers on a nonexistent view. (Codex review P2.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The dispatch_event table (migration 20260523055641) was created relying on
ALTER DEFAULT PRIVILEGES to reach windmill_user/windmill_admin. Those default
privileges only apply to objects created by the role that set them
(20250205131523), so deployments whose migration runner is a different role
leave dispatch_event ungranted. Direct writes then run as the invoking role and
fail with "permission denied for table dispatch_event" -- notably the DELETE in
delete_jobs (windmill-common/src/jobs.rs) that reaps a job's side rows on
schedule disable, and the dispatcher insert in asset_dispatch.rs.
Grant explicitly, same fix as notify_event (20260619091631) and script_trigger
(20260619112847). GRANT is idempotent so re-application (squash, or an operator
who already granted manually) is a no-op.
Fixes WIN-2112
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ansible): support repo-provided ansible.cfg in delegate_to_git_repo
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ansible): accept colon delimiter and collections_paths alias in cfg parser
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* [ee] test(audit): de-flake S3 export end-to-end test under parallel tests
Bumps the EE ref to pull in the companion fix for the flaky
`audit_export_end_to_end` test (`ee::audit_s3_export`). Postgres XIDs and the
snapshot xmin are cluster-wide, so under `--test-threads` a neighbor test's
in-flight transaction can hold the global xmin between this test's row xids,
deferring a committed row to a later export tick (`id 7 must be exported: got
[3,4,5,6]`). The EE change models successive ticks (drain until exported) and
waits for pre-existing rows to settle before anchors that must exclude them.
Test-only; no production code changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to bdd4ba4dfd05c8ca7db365e530812dc914517d7f
This commit updates the EE repository reference after PR #639 was merged in windmill-ee-private.
Previous ee-repo-ref: 70c4c61257bda9263c158ef0ac58eb3aa9c55fa8
New ee-repo-ref: bdd4ba4dfd05c8ca7db365e530812dc914517d7f
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix: enforce tls verification for postgres verify-ca/verify-full sslmode
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: make PG_ACCEPT_INVALID_CERTS value-based and keep cache key well-formed
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: grandfather existing postgres resources via per-resource trust_cert flag
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: scope pg trust_cert migration to resource data, drop schema patch
Hub resource type sync (windmill cache-rt + startup SYNC_CACHED_RT) only touches the admins workspace and is opt-in, so the schema is left to the hub; the migration just grandfathers existing resource values so the upgrade is non-breaking.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: replace pg trust_cert with verify-*-scoped accept_invalid_certs, drop migration
Per-resource accept_invalid_certs (default false for new resources) replaces the trust_cert flag and grandfather migration. It only applies to verify-ca/verify-full; unset falls back to legacy behavior (verify only when a root cert is present) so existing and git-synced resources are not broken on upgrade.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: warn in job logs when a verify-* postgres resource skips cert verification
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* [ee] feat(licensing): enforce offline license seat cap
Companion to windmill-ee-private. Aligns the offline-license seat count
with the billing model and adds real-time enforcement when usage exceeds
the cap. OSS side carries the ee_oss stubs, the reactivation cap-check
call site, the regenerated SQLx cache, and the EE ref bump.
- Exclude instance-disabled users (password.disabled) and service
accounts from the seat count. Deactivating a user now frees a seat.
- Service accounts no longer consume seats (no check at creation).
- Hard-block reactivation when it would exceed the cap.
- Invalidate the license (halting jobs) when seat usage exceeds the cap,
mirroring CU-cap enforcement; recovers when usage drops back under or a
higher-cap key is loaded.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* [ee] fix(licensing): bump EE ref for reactivation seat-check fixes
Points to the EE companion commit that fixes reactivation double-counting
and preserves the original seat alert tag.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* [ee] fix(licensing): reactivation seat delta includes pending invites
Bumps the EE ref and drops the now-orphaned usr-only cache entry; the
reactivation check reuses the existing usr ∪ workspace_invite query.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* [ee] test(licensing): bump EE ref for offline seat-cap tests
Adds #[sqlx::test] coverage for the offline seat counting and cap-check
logic; EE-only (runtime queries, no cache change).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: update ee-repo-ref to f814c3f75308c1ef1e4526d8d0eeb360ce16abe4
This commit updates the EE repository reference after PR #637 was merged in windmill-ee-private.
Previous ee-repo-ref: b2622e3afc2fe1fe3e2ec978ca46cf9decf91b82
New ee-repo-ref: f814c3f75308c1ef1e4526d8d0eeb360ce16abe4
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.8 <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>
* [ee] fix(s3_proxy): preserve URL-encoding on forward re-sign for Hive-partition keys
Bump ee-repo-ref to pull the EE fix for SigV4 SignatureDoesNotMatch on
DuckLake Hive-partition writes through the S3 proxy. The forward re-sign
leg rebuilt the upstream URI from the decoded object key (literal `=`)
instead of the still-encoded request path (`%3D`), diverging from how
S3/minio canonicalizes the key. Companion EE commit c6b110f.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 1a98119b0b8b8548601983c9e5ab091150f0b180
This commit updates the EE repository reference after PR #638 was merged in windmill-ee-private.
Previous ee-repo-ref: c6b110fd3b3591a5c3f09952c388c42bd5766188
New ee-repo-ref: 1a98119b0b8b8548601983c9e5ab091150f0b180
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat: add dev workspaces paired with a lockable prod workspace
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: gate dev-workspace prod-lock on admin and prevent attach cycles
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: redirect locked-prod edits into the dev workspace
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: make dev-workspace settings tab available on CE (was EE-gated)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: lock prod against forking too and funnel edits to the dev workspace
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: open dev item page on edit and tailor dev-workspace lock messages
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: prevent nested dev workspaces and hide dev option when one exists
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: drop the redundant already-has-dev hint on the fork form
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: badge dev workspaces and sort them ahead of forks in the tree/switcher
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: label dev workspaces as 'Dev workspace of X' instead of 'Fork of X'
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: label edit as 'Edit in <dev>', cover editor headers, auto-expand dev in tree
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: split prod lock into separate block-deploy and prevent-forking toggles
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: make resources/variables workspace-specific from compare page
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: steer AI-chat sessions to the dev workspace
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: refine session fork options and lock guidance for dev/prod
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: session picker reads prod's real rules, default to current ws
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: copy members into forks and clarify dev-workspace root labeling
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style: place the workspace id field under the fork name
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: address dev-workspace review findings and harden fork detection
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: regenerate sqlx offline cache
Restores entries dropped during the origin/main merge and adds the
dev-workspace queries (is_dev_workspace, ws_specific, has_parent).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: address second-round dev-workspace review findings
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: address Pi and Codex review findings on dev-workspace endpoints
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: gate locked-dev git-branch fork on admin and validate ws_specific path
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: clear prod dev-lock when deleting an attached dev workspace
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor: consolidate dev-workspace migration and scope all-group join to attach
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: restore dev-workspace CHECK into consolidated migration and scope all-group join
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor: drop copy_members from the dev-workspace attach path
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: dev-workspace lifecycle/auth fixes from Codex review round
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: explicit create-in-other for workspace-specific items
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: make create-in-other strictly create-only (never overwrite target)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: return 403 (not 401) for dev-workspace permission denials
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: allow attaching a same-family fork as a dev workspace
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style: emphasize the go-to-dev action in the no-direct-deploy alert
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: seed a resource's linked variables when creating it in the other workspace
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: judge workspace deploy/fork locks against the user's identity in that workspace
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style: clarify create-in help text in workspace-specific panel
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: admin-gate dev-workspace creation and harden lock/seed edges
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: preserve a staged fork's source on picker create-mode re-entry
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: clear dev flag on archive and check dev existence server-side
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: make create-in-other atomically create-only via direct create
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: create-only resource insert, ws-specific list scopes, archive lock guard
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: reserve the dev_workspace_lock protection-rule name from the public API
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: reattach create_protection_rule doc comment to its function
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor: make dev-archive pairing teardown atomic with the archive
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: follow deploy_to on root rename; show dev pairing to non-member prod admins
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: copy creator metadata on fork; invalidate fork routing cache on rename
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: accept g/ paths in set_ws_specific; gate copy_members to dev workspaces
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
build_gcs_client always called `.with_service_account_key(...)`, so an
absent key (the settings UI stores "no key" as the empty JSON object `{}`)
was handed to the builder and failed to parse instead of falling through
to the object_store crate's InstanceCredentialProvider. Skip the call when
the key is blank so GCS uses the instance's ambient credentials (GKE
Workload Identity / the GCP metadata server).
"Blank" (empty/whitespace/`{}`/`null`) is centralized in a shared
`gcs_service_account_key_is_blank` predicate so the build path and the
non-super-admin connectivity-test SSRF guard (`validate_object_storage_test`)
agree on what counts as "no key" — otherwise a blank key would bypass the
guard yet still trigger the ambient-credential fallback, letting an
untrusted caller probe arbitrary buckets with the server's instance role.
Also clarify the settings UI hint that the key may be left empty for
ambient credentials, and add regression tests for the blank-key build path
and the guard.
Fixes WIN-2110
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MermaidDisplay only showed the rendered SVG, hiding the raw source once
rendering succeeded. Add a copy button in the showSvg branch mirroring the
pattern in HighlightCode.svelte so the diagram source can be extracted.
Fixes WIN-2109
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(gcp): require token verification for authenticated push delivery
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 38e87caeca6a1dce9e4f3fa029ac36dffb30f1b2
This commit updates the EE repository reference after PR #636 was merged in windmill-ee-private.
Previous ee-repo-ref: 8c63d487c486002baf09c77ab937fd77a91765eb
New ee-repo-ref: 38e87caeca6a1dce9e4f3fa029ac36dffb30f1b2
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat(pipeline): AI chat tools to build pipeline nodes with diff/approval
Add a data-pipeline AI chat experience modeled on the flow editor and
surfaced through the dev-gated global chat (no new chat panel).
The /pipeline editor registers PipelineAIChatHelpers on the AIChatManager;
while it is open the global mode layers pipeline tools, a pipeline prompt
section, and the helpers on top of the full global tool set (behavior is
unchanged when no pipeline editor is open).
New tools (frontend/src/lib/components/copilot/chat/pipeline/core.ts):
- get_pipeline_graph / read_pipeline_node — read the live graph and bodies
- build_pipeline_node / edit_pipeline_node — stage changes as AI-pending drafts
- remove_pipeline_node — drop a staged proposal
- test_pipeline_node — preview-run a node (requires confirmation)
Tools never deploy: they stage drafts flagged aiPending, rendered on the
canvas with an accent ring and reviewed via Accept all / Reject all (the
flow editor's GlobalReviewButtons). Accept commits the drafts; Reject reverts
to a pre-AI snapshot, preserving earlier accepted drafts. Auto-accept is gated
on the chat autonomy mode.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(pipeline): teach the global/session chat to author data pipelines
Without an open /pipeline editor the session chat had no pipeline concept, so
"create a data pipeline" loaded flow instructions and built a flow. Add a
first-class pipeline authoring path:
- system_prompts/base/pipeline-base.md — what a data pipeline is (a DAG of
annotated scripts wired by storage assets, NOT a flow) and how to author the
// pipeline / // on / // materialize annotations; wired through generate.py as
getPipelinePrompt() (regenerated prompts.ts/index.ts).
- global/core.ts — new get_instructions subject "pipeline", and a global-prompt
rule disambiguating data pipelines from flows so the model routes correctly.
- ai_evals/cases/global.yaml — two global cases (single node, two-node chain)
asserting pipeline-annotated script drafts and forbidding write_flow, guarding
the pipeline-vs-flow conflation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(pipeline): show & build pipelines in the AI session preview
Add a 'pipeline' session preview target so the session AI can show the
data-pipeline graph for a folder and build nodes in-pane:
- open_preview now accepts kind="pipeline" (path = folder); SessionTarget /
EDITOR_TARGET_KINDS widen accordingly. The slot/codec load model stays
flow|script|raw_app — pipeline bypasses it with its own fetch/draft state.
- New PipelineEditorView.svelte mounts in the session pane: fetches the
folder graph, overlays AI drafts, renders AssetGraphCanvas + the
Accept/Reject review buttons, and registers PipelineAIChatHelpers on the
*session-scoped* manager (via getAiChatManager) so build_pipeline_node /
edit_pipeline_node + the diff/approval work inside the session too.
- System prompt nudges the model to open the pipeline preview and use the
staging tools while building.
Verified end-to-end with a real model: the session AI called open_preview,
the graph mounted in the side panel, then build_pipeline_node staged a node
on the session canvas with its schedule trigger and ducklake output.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(pipeline): share the AI editor logic between route page and session
Consolidate the duplicated data-pipeline AI logic onto a single shared layer so
the route editor and the in-session preview behave identically and the session
gains the full code editor.
- New pipelineAiHelpers.ts: createPipelineAiHelpers(deps) owns the propose/edit/
remove/accept/reject/test staging + the per-turn snapshot bookkeeping that
powers Reject. Callers inject accessors for their own draft Map and graph.
- Route page (/pipeline/[folder]) drops its ~250-line inline AI-helper block and
wires the shared factory via deps (folder/workspace/graph/drafts + focus,
ensureEditable, run-started). Its shell — persistence, navigation guard,
activity, cascade, trigger drawers — is untouched.
- Session PipelineEditorView uses the same factory and now renders the real
AssetGraphDetailsPane (code editor + live overlays + test), so a node built in
a session opens with its source, matching the route editor.
Verified: route page hydrates/renders drafts unchanged; in a session the AI
opened the pipeline preview, built a node, and its code showed in the details
pane. check:fast clean, 197 unit tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(pipeline): externalize editor state into PipelineEditorState (step 1)
Introduce PipelineEditorState — the data-pipeline analogue of the flow editor's
flowStore. It owns the draft Map, the live editor overlays, and the selection,
with callback-safe methods (handleDraftPersist / handleAnnotationsChange / … ),
so a single editor can be rendered by both the route page and the session.
This commit lands the store and points the in-session PipelineEditorView at it
(no behaviour change — the session already had these inline). Next steps move the
route page onto the store and a shared <PipelineGraphEditor>.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(pipeline): point the route editor at PipelineEditorState (step 1)
Move the route page's draft Map, live editor overlays, selection, and the
draft-persist / live-change handlers onto the shared PipelineEditorState (`pe`),
referencing them as `pe.*` in place. No behaviour change — persistence, graph
resolution, run dispatch, AI staging, and deploy all stay on the page and now
read/write the externalized state.
This is the data-pipeline analogue of the flow editor's flowStore: the route
page and the in-session preview now share one source of editor truth, setting up
the shared <PipelineGraphEditor> in the next steps.
Verified: the page hydrates its DB draft, renders the overlay graph, the toolbar
counts (Save all (N)) track pe.drafts, and selecting a node opens it in the
details pane. check:fast clean, 84 unit tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(pipeline): render the route editor via shared PipelineGraphEditor (step 2)
Extract the canvas + details-pane editor body into PipelineGraphEditor.svelte,
the data-pipeline analogue of FlowBuilder. The route page now delegates its
Splitpanes block to it, passing the externalized PipelineEditorState plus its
run/cascade/trigger/deploy callbacks; the component owns pane sizing,
selection/details-open derivation, and the canvas+details rendering.
Root-caused the earlier ts2769 "$props() No overload" to a prop named `state`
colliding with the `$state` rune (`let x = $state(...)` parsed as a store
auto-subscription on the prop) — the prop is now `editor`.
Net: the route page sheds ~310 lines of template/state; behaviour preserved.
Verified: the page hydrates its DB draft, renders the graph, opens the draft in
the details pane (live code editor + Test), pane sizing works. check:fast clean,
24 pipeline tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(pipeline): move draft autosave into PipelineGraphEditor (step 3)
Fold the per-user `data_pipeline` DraftService bundle autosave (hydrate +
debounced persist + localStorage crash mirror) into PipelineGraphEditor, gated
by a `persistDrafts` prop — FlowBuilder's parameterized-autosave shape. The route
page passes `persistDrafts` + `folder` and reads `editor.loadedFromDbDraft` for
its AutosaveIndicator; the in-session preview will leave persistence off.
Also restores the `untrack(...)` wrapping on the pane-sizing $effect (dropped
when the editor body was extracted in step 2). Without it the Pane `bind:size`
feedback loops the effect and pegs the main thread when the details pane is
closed — a latent hang in the step-2 commit.
check:fast clean, 24 pipeline tests pass. Note: browser revalidation was not
possible this session (the Playwright MCP browser was reset); the autosave is a
verbatim port and the untrack fix is the original working form.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(pipeline): render the session preview via shared PipelineGraphEditor (step 4)
Point the in-session PipelineEditorView at the shared PipelineGraphEditor instead
of its own inline canvas + details pane. The session now renders the exact same
editor body as the route page — gaining the full details/code pane — while opting
out of persistence (persistDrafts=false) and the run/cascade/trigger/bounded
affordances (their callbacks are omitted, so those controls hide). Building nodes
+ the Accept/Reject diff still work via the AI helpers.
Also fixes issues surfaced by a full `svelte-check` while wiring this up:
- PipelineGraphEditor: edit mode opened the details pane unconditionally (a step-2
regression); restored the route's "open only on selection/draft" behaviour.
- Route page passed an `isOperator` prop the component doesn't accept (step-2;
caught only by full check, not check:fast).
- SessionItemNotFound: narrow its `kind` to exclude `pipeline` (pipeline targets
never slot-load, so they can't 404 through it) — closes the SessionTarget-widen
fallout.
- PipelineEditorView: cast the resolveGraph base to AssetGraphResponse.
Full `svelte-check` now clean across all pipeline/session files; 137 unit tests
pass. (Browser revalidation still pending — Playwright MCP was unavailable this
session; see the smoke-test note on the PR.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): stop an infinite microtask loop when persisting a no-output draft
handleDraftPersist short-circuits when the open draft's content + inferred writes
are unchanged. The writes check compared `d.outputAssets?.length === writes.length`,
but a no-output draft has `outputAssets: undefined` (so `?.length` is `undefined`)
while the details pane infers an empty `writes: []` (length 0). `undefined === 0`
is false, so it never short-circuited: every persist re-wrote the drafts Map with
an equivalent object, which gave `activeDraft.script` a new identity → the pane
re-emitted its overlays → the graph re-derived → persist fired again. A self-
sustaining microtask loop that pegged the renderer and froze the tab on any
pipeline carrying a no-output draft (e.g. hydrating one from the saved
data_pipeline draft on load). It hangs rather than throwing effect_update_depth_
exceeded because it cycles across microtasks, not within one reactive flush.
Fix: coalesce the undefined length to 0 so "no outputs" compares equal to an empty
inferred-writes list. Adds pipelineEditorState.test.ts covering the idempotency
(fails without the fix) plus the change/no-change cases.
Root-caused by instrumenting the reactive churn: every iteration reassigned
drafts/liveContent/liveBodyAssets/liveAnnotations/displayGraph with identical
values — pure reference churn off the drafts re-write.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): make the agent open the pipeline editor before building nodes
In a session, the GLOBAL system prompt only *advised* opening the pipeline preview
("show its graph with open_preview ... prefer those tools once it is open"), so
the agent routinely skipped it: on a plain "build a data pipeline" request it
reached for write_script and staged plain script drafts, and the canvas editor
never opened. build_pipeline_node / edit_pipeline_node are only registered once
the preview is open, so skipping open_preview also loses the canvas-staged
Accept/Reject diff-approval flow entirely.
Make the guidance imperative: open_preview(kind="pipeline", path=<folder>) is the
FIRST step before creating any node (an empty or not-yet-created folder is fine —
create_folder first if needed), and pipeline nodes go through build_pipeline_node
/ edit_pipeline_node, never write_script. This also clears the agent's "the folder
might not exist" hesitation that pushed it toward write_script.
Verified live (same plain prompt, before/after): before it used write_script with
no editor; after, the agent opens the editor first and stages a canvas-highlighted
node with Accept all / Reject all. The guidance is gated on previewTools
(session-only), so it doesn't affect the non-preview global eval cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pipeline): preserve in-session pipeline drafts across editor hide/show
The session preview's PipelineEditorState lived in the PipelineEditorView
component with persistDrafts=false. Hiding the editor sets editorVisible=false,
which makes `hasEditor` false and the `{#if hasEditor}` block unmount the view —
discarding its component-local store. Showing it again remounted a fresh, empty
one, so the pipeline the AI had built in the session vanished.
Move the PipelineEditorState onto the per-session SessionRuntime (like the flow /
script / raw_app editors, which already host their state there and take {runtime}),
so it survives the pane unmount on hide and across session switches. The runtime
is keyed by session id and only dropped on session deletion.
Because the instance is now reused, guard against a retarget to a different
folder: PipelineEditorView resets the state when `path` changes to a new folder
(a same-folder remount keeps the drafts). Adds `folder` + `reset()` to the store.
Verified: build a node in a session → Close editor → Show editor → the staged
node, its wiring, the details-pane code, and Accept/Reject all re-appear. Full
svelte-check clean; 139 pipeline tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(pipeline-ai): clearer diff + persistent review banner on the canvas
The AI review affordance had two problems on the pipeline canvas:
- The floating Accept-all / Reject-all bar sat bottom-center, where it
collided with the minimap once the canvas narrowed on node selection —
reading as "the buttons vanished when I select a node".
- Every staged draft rendered with the same blue ring, so it wasn't clear
what the review would actually change (a plain manual draft looked the
same as an AI proposal).
Replace the floating bar with a top-left review banner (z-30, clear of the
controls and minimap) that stays put regardless of selection and spells out
the pending counts. Color the diff per node: a proposal that adds a node
that isn't deployed rings green with a "new" chip; one that edits an
already-deployed node rings amber with an "edited" chip. Plain manual
drafts keep the neutral gray dashed border, so only the green/amber nodes
read as part of the Accept/Reject set.
aiPendingKind is resolved in resolveGraph (deployed runnable present →
modified, else added) and forwarded through the canvas to the node.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipeline-ai): persist in-session pipeline proposals across reload/switch
Staged AI proposals lived only in the per-session runtime's in-memory
PipelineEditorState (persistDrafts=false), so a page reload — and an
LRU-evicted runtime on session switch — dropped them, leaving the canvas
and the Accept/Reject review empty even though the chat still showed the
nodes as staged.
Enable the same per-folder DB-draft persistence the route page uses for the
in-session editor. To keep hide/show cheap and race-free, hydration is now
gated per editor instance (PipelineEditorState.hydratedFromDb) rather than
per component mount: the runtime-hosted instance hydrates ONCE when fresh
(reload / evicted runtime) and then keeps its in-memory drafts across the
editor pane unmounting on hide — re-reading the DB on every remount would
race a not-yet-flushed autosave and drop a just-staged draft. A folder
retarget resets the flag so the new folder re-hydrates.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipeline-ai): make Reject all work for rehydrated proposals
rejectAll only reverted paths tracked in the in-memory aiSnapshots map,
which is rebuilt empty on each editor mount. After a reload (or session
switch into a fresh runtime) the proposals are restored from the persisted
draft but have no snapshot, so Reject all was a no-op on exactly the nodes
it should discard. Sweep any still-pending draft without a snapshot and
discard it (revertPath with no snapshot deletes the path; for an edit of a
deployed node that correctly falls back to the deployed body). Adds unit
coverage for accept/reject including the no-snapshot case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipeline-ai): keep proposals visible while the graph reloads on switch
The session editor pane is LRU-capped (MAX_WARM_EDITORS), so returning to a
session whose pane was evicted remounts PipelineEditorView with a fresh
graphRes resource (loading=true, current=undefined). The deployed-graph
loading spinner gated the whole canvas, so the staged proposals and the
Accept/Reject review banner vanished until the re-fetch resolved — read as
"the proposal disappears when I switch sessions".
Only show the loading/error placeholder when there are no drafts to display.
When the runtime already holds staged drafts, render the editor immediately:
resolveGraph overlays them on an empty base so the proposals + banner stay
visible, and the deployed nodes fill in when the fetch completes. Verified
with a 4s-delayed graph fetch — proposals render through the load with no
spinner.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(pipeline-ai): apply AI node edits directly as drafts, no approve/reject
The canvas-level Accept all / Reject all review (aiPending proposals, the
green/amber diff ring + "new"/"edited" chips, and the review banner) didn't
fit the pipeline editor. Match the flow/script editor instead: build/edit
apply directly as ordinary unsaved drafts on the canvas, which the user then
deploys — there is no separate approval step.
Removed across the surface:
- aiPending / aiPendingKind on the runnable node + resolveGraph seeding +
canvas forwarding; AI-built nodes now render with the existing plain
unsaved-draft dashed styling.
- the review banner, count derivations, and hasAiPending/onAccept/onReject
props from PipelineGraphEditor and both consumers (route page + session
view).
- acceptAll/rejectAll/hasPending and the per-turn snapshot bookkeeping from
the shared helpers; removeProposedNode now just discards the unsaved draft
at a path (undo a build). acceptAllProposals/rejectAllProposals/
hasPendingProposals dropped from the PipelineAIChatHelpers interface and
the manager's auto-accept hook.
- accept/reject language from the tool descriptions, return messages, and the
system-prompt section.
Tests updated; pipeline + AssetGraph suites pass (142).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(drafts-diff): support data_pipeline diffs + fix blank empty-summary row
Two issues in the session "Drafts" diff drawer (DraftDiffDrawer):
- Clicking a `data_pipeline` bundle row threw "Draft diff not supported for
kind data_pipeline" (utils_draft_deploy.ts) — there was no handler for the
kind, so it fell to the OVERLAY_GETTERS lookup and errored. The bundle has
no deployed counterpart (each node deploys individually as a script), so
diff it node-by-node: surface each node's draft body keyed by path, folding
in the deployed body as the "before" when a node edits a deployed script.
- A draft row whose summary is an empty string (e.g. the app draft) rendered
with no title at all: WorkspaceItemRow's single-line branch used
`summary ?? secondary`, and `??` doesn't treat '' as absent, so it showed
the empty summary instead of the path. Use `||` so an empty summary falls
back to the path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(drafts-diff): explode data_pipeline bundle into per-node subitems
A data_pipeline draft is a bundle of node-script drafts, so a single row
diffed the whole thing as one blob. Explode it in DraftDiffDrawer into one
script row per node, nested under the bundle's `…/data_pipeline` folder so
they read as the pipeline's subitems — each with its own path and a proper
script Content/Metadata code diff. The node's draft body is the "after"; its
deployed body (when the node is already deployed) is the "before", so edits
show as line diffs and new nodes as added. A single bundle row (via the
getDraftDiffValues data_pipeline fallback) is kept only for the case where
the bundle can't be read.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(pipeline-ai): simplify — drop vestigial approve/reject scaffolding & redundant field
Review pass over the PR, removing complexity left from the approve/reject
removal and the shared-component refactor (all behavior-preserving):
- Inline the `acceptPendingEdits` pass-through into `acceptPendingFlowEdits`
and revert the now-inert `autoAcceptEditsAvailable` GLOBAL+pipeline widening
(pipeline edits are direct drafts — nothing to auto-accept).
- Fix the global system prompt: pipeline tools "apply directly as unsaved
drafts (no accept/reject)", not "proposals the user Accepts or Rejects".
- Collapse the redundant `outputAsset` (singular) into `outputAssets`,
removing a whole resolveGraph fallback tier; simplify propose/editNode.
- Drop the single-field `PipelineAiHelpersHandle` wrapper (callers just
destructured `{ helpers }`); inline the misleading `isoNow()` helper.
- Remove the now-unreachable `data_pipeline` branch in getDraftDiffValues
(the drafts drawer explodes bundles per-node; an unreadable bundle is
skipped) and the "Step N consolidation" drafting narration.
- Un-export internal-only types; reuse `storageKey`; refresh stale comments
that still referenced proposals / the review banner / diff-approval.
svelte-check clean; 141 unit tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(pipeline): tooltip clarifying the Create/Save button deploys
The accent button in the asset-graph details pane ("Create" for a new script,
"Save" for an existing one) is really a deploy, but had no tooltip explaining
that. Add a title — "Deploy this new script to the workspace" / "Deploy your
changes to this script" — keeping the create-vs-update label distinction while
making clear both deploy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(pipeline-ai): document the `materialize` annotation in the pipeline prompt
The model invented "materialize run" because the prompt only mentioned
`// materialize <uri>` in passing. Spell out what it is in both the in-app
pipeline prompt (getPipelinePromptSection) and the base prompt
(pipeline-base.md, regenerated): a MANAGED output where the runtime writes the
table around a single SELECT (no manual CREATE/INSERT); replace (default) vs
`append` vs `key=<col>` strategies; `manual` to opt out (track-only); and its
pairing with `// partitioned …` (runs once per partition, `{partition}` token
substituted at run time). Explicitly: materialize is an output declaration,
not a command — there is no "materialize run".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(pipeline-ai): trigger drawers in the AI session preview
Bring the route page's native-trigger affordances to the in-session pipeline
editor by reusing the shared <PipelineTriggerEditors> (no duplication of the
drawer UI). Clicking a "Schedule · Missing — no trigger row" node (or
edit/delete on an attached trigger, webhook, data-upload) now opens the same
drawers the full editor uses, instead of doing nothing. Draft nodes get the
same "save the script first" guard (a trigger row needs a deployed script).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(pipeline-ai): run buttons + live run state in the AI session preview
Wire the per-node Run button and live run-state badges into the in-session
pipeline editor, reusing the shared folder-scoped job poll
(useActiveRunnableIds) the route page uses — node badges, the event log, and
the zero-latency "running" hint all come from it. The session runs one node at
a time (preview for an unsaved draft, the deployed version otherwise),
skipping the route page's cascade/deploy-queue machinery the AI-session UX
doesn't need. Verified: a node's Run button dispatches a job and the badge
updates live from the poll.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(pipeline): label the node deploy button "Deploy" (was Create/Save)
Users read "Create" and asked whether it deploys. It does — and the main
script editor's DeployButton already says "Deploy", so this is the consistent
term. Use "Deploy" for both the new-script and existing-script cases; the
new-vs-changes nuance stays in the button's tooltip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(home): surface data pipelines as units, including bundle-phase drafts
Treat a data pipeline as one home entry instead of scattering its member
scripts:
- The home "Pipeline · f/<folder>" entry now also covers bundle-phase
pipelines — a folder that so far only exists as a `data_pipeline` draft —
not just deployed ones, so a pipeline shows up the moment its first node is
drafted (union listPipelineFolders + data_pipeline draft folders).
- Pipeline-member scripts (`auto_kind='pipeline'`) are filtered out of the
individual scripts list; they're represented by their pipeline's entry.
- Tree view injects pipeline folders so they (and their "Pipeline" entry)
still appear when their only scripts are hidden members or they have none
deployed yet.
Verified in both list and tree view: app_groups (deployed member folded) and
a draft-only nyc_transit both show as pipelines; the member script no longer
lists individually.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(scripts): compute auto_kind for draft-only pipeline nodes
A never-deployed pipeline node (a script draft starting with `// pipeline`)
had no script row, so list_scripts synthesized it with `auto_kind: None` — and
the home page therefore couldn't tell it was a pipeline member, listing it
individually instead of folding it into its pipeline. Parse the draft content
the same way the create path does (`parse_pipeline_annotations(...).in_pipeline`)
and set `auto_kind = "pipeline"` on the synthesized draft-only row, so draft
nodes fold into their pipeline like deployed members.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(search): hide pipeline-member scripts from global search
The Ctrl+k global search listed pipeline-member scripts (`auto_kind='pipeline'`)
individually. Filter them out — they're reached through their pipeline, matching
the home page. Deployed members carry auto_kind from the script row; draft-only
members now do too (computed from draft content in list_scripts), so both are
excluded here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipeline): address PR review findings
Session run dispatch (the one real bug):
- runNode now passes `_wmill_skip_asset_dispatch: true` for a single-node run
of a deployed node unless the user chose "run + downstream" (cascade) —
previously a single Run could fan out to downstream deployed scripts via the
backend asset dispatcher and fire side-effecting production runs.
- onRunProducer guards `kind === 'script'`; onTestStateChange only clears the
run hint for the script the pane finished (not a different in-flight node);
clear the hint on folder retarget; gate the background poll on isActiveSession
so hidden warm panes don't poll; note the PipelineTriggerEditors workspace
coupling.
Home page pipeline surfacing:
- Fold pipeline-member folders into `pipelineFolders` (captured in loadScripts)
so a members-only / draft-only-`// pipeline` folder still shows its pipeline
entry instead of vanishing; and don't render the empty-state when only
pipelines remain (they aren't part of the text filter).
- Insert injected tree folders in name order instead of prepending.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(pipeline-ai): make clear `// materialize` is DuckDB + DuckLake only
The model put `// materialize` on a python3 node, which deploy rejects ("only
supported for DuckDB scripts"). The prompt only implied SQL ("write the body
as a single SELECT") without stating the hard constraint. Spell it out in both
the in-app prompt and pipeline-base.md: `// materialize` is DuckDB-only and its
target must be a DuckLake table; for python3/bun/postgresql nodes, write the
output via the SDK instead and let it be inferred — reach for duckdb when a
node should materialize a DuckLake table.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(pipeline-ai): fix stale comment — session now wires run + trigger affordances
Addresses review: the comment still claimed the session 'opts out of the
run/cascade/trigger/bounded affordances', but run buttons + trigger drawers
were wired in. Describe the current state (wires run + triggers; omits only
cascade/bounded/add-script).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipeline): address Codex review — test_pipeline_node dispatch + tree search
- [P1] testNode (the test_pipeline_node tool) ran a deployed node via
runScriptByPath without `_wmill_skip_asset_dispatch`, so previewing one node
could fan out to downstream deployed subscribers and run side-effecting
scripts. Add the skip flag (test is always single-node) + a regression test.
- [P2] Home tree view injected pipeline folders — and rendered their Pipeline
row — even during a text search, surfacing unrelated pipelines. Gate both the
TreeViewRoot injection and TreeView's hasPipeline on `!isSearching`, matching
the list view which hides pipeline rows on a query.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipeline-ai): keep the pipeline prompt after update_user_instructions
rebuildGlobalSystemMessage (called by the update_user_instructions tool)
rebuilt only the base Global prompt, dropping the pipeline-editor section that
configureGlobalMode appends. So after the chat remembered an instruction, the
next GLOBAL turn lost the active /pipeline/<folder> context + direct-draft/
materialize guidance while pipeline tools stayed registered. Re-append the
pipeline section here when a pipeline editor is registered.
Addresses Codex review [P2].
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(home): gate pipeline entries by kind/archived/owner filters
Codex review [P2]: pipeline rows/folders rendered independently of the item
filters, so a pipeline still showed under the Flows/Apps tabs, in the archived
view, and outside a selected owner. Add `visiblePipelineFolders` applying the
same gates the items get (kind ∈ {all, script}, not archived, owner-prefix
match) and route the list rows, tree injection, and empty-state check through
it. Pipelines are always `f/<folder>`, so the user-folder toggle and kind=script
keep including them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipeline): address review — route folder-switch state, AI node guards, diff identity
claude[bot] [P1]: the route page's in-app folder switcher navigates same-route
(no remount), but nothing reset PipelineEditorState — so folder A's drafts
displayed under B and autosave persisted them into B's bundle, and B never
hydrated. Reset pe on folder change (mirror the session retarget), and guard the
shared hydrateDrafts against a stale folder result landing after a retarget.
codex/claude [P2]: build_pipeline_node (proposeNode) only checked drafts.has —
now rejects a path outside the open folder and one colliding with an existing
deployed node (model should edit_pipeline_node). + 3 regression tests.
codex/claude [P2]: exploded pipeline-node diff rows shared `script/<path>` with a
standalone script draft at the same path, colliding in the {#each} key + value
cache. Add an explicit unique `key` (the distinct bundle-nested path) on DiffRow;
pipeline nodes set/look up by it while `path` stays the real edit target.
claude [P2]: session AI test_pipeline_node now arms the live run badge
(onRunStarted), matching the route page.
nit: pipelineAiHelpers.test uses afterEach(restoreAllMocks) instead of an
unreachable inline mockRestore.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipeline): harden AI node mutations + close home label-filter / rename gaps
Codex [P1] (AI mutations trust model paths) — fully scoped now:
- editNode validates the open folder too (proposeNode already did), via a shared
assertInFolder; an edit_pipeline_node for f/other/* no longer persists an
unrelated script into the current folder's data_pipeline bundle.
- both build_pipeline_node and edit_pipeline_node now require the `// pipeline`
annotation (assertPipelineAnnotation) so a staged draft is definitionally a
pipeline member, not a silently-non-member script. + tests.
(proposeNode's folder + deployed-collision guards landed in the prior commit.)
Codex [P2] home label filter — visiblePipelineFolders ignored labelFilter, so a
label selection still showed every pipeline (and the empty-state fell through to
render pipeline rows). Pipelines carry no labels, so a label filter hides them.
Codex [P2] session rename — PipelineEditorView now wires onScriptRenamed
(repoint selection + refetch), matching the route page; a persisted-script
rename no longer leaves the canvas on the old path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(pipeline-ai): language-specific comment prefix for annotations
Codex [P2]: the tool schema and prompt told the model to write `// pipeline` /
`// on` / `// materialize` regardless of language, and pipeline-base.md grouped
SQL with `#`. A `//` (or `#`) annotation line is invalid in a DuckDB/Postgres
node — it passes the frontend parser (which strips `//`/`--`/`#`) but is a SQL
syntax error at deploy/run. Make the guidance language-specific everywhere:
`--` for SQL (duckdb/postgresql), `#` for python3/bash, `//` for bun/TS — the
`//` in examples is the TS form to translate. Regenerated the prompt outputs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipeline-ai): re-scope Global prompt on folder switch + language-aware base prompt
Codex [P2] x2:
- The route page resets editor state on an in-app folder switch, but the Global
chat's system message kept the old `/pipeline/<folder>` scope (the helper
methods read the reactive folder, but the prompt string is only rebuilt on
Global-mode reconfigure). Rebuild it on folder change so the next turn targets
the new folder.
- The pre-editor base Global prompt (seen before open_preview/get_instructions)
still showed TS-only `// pipeline` / `// on`. Make it language-aware (`--` SQL,
`#` Python/Bash, `//` TS) so the model can't draft invalid DuckDB/Postgres
nodes before the pipeline tools are registered.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipeline-ai): authoritative new-node probe + SQL-correct eval checklist
Codex [P2] x2:
- build_pipeline_node's collision check relied on the resolved graph, which can
be empty while the session preview races open_preview (a build could shadow a
deployed node before the graph loads) and only covered pipeline runnables, not
a non-pipeline script at the same path. Add an authoritative backend probe
(ScriptService.getScriptByPath): any deployed script at the path → reject with
"use edit_pipeline_node". + regression test (empty graph, deployed script).
- The DuckLake eval judgeChecklist required the exact `// pipeline` annotation,
which would penalize the now-correct `-- pipeline` SQL output (or reward
invalid DuckDB syntax). Make both cases syntax-aware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipeline-ai): rebuild Global prompt on session preview folder retarget
Codex [P2]: open_preview(kind="pipeline", path="B") can retarget an existing
pipeline preview from folder A to B without remounting. The retarget effect
resets editor state and the helper methods read the new path, but the
registration effect only depends on isActiveSession, so the Global system
message stayed scoped to /pipeline/A. Mirror the route-page fix: rebuild the
global system message on retarget (gated on isActiveSession — only the active
session's helpers are registered; a hidden session reconfigures when it next
becomes active).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipeline-ai): edit_pipeline_node preserves deployed script metadata
Codex [P1]: editNode kept only the deployed script's language and staged a fresh
makePipelineScript draft with empty hash/summary/description/tag/schema/settings.
Deploying that edit from the pane (auto_parent) would update the script while
wiping its metadata, and the route "Save all" path (no parent_hash) could hit
the backend path-conflict branch on the occupied path. Base the draft on the
existing draft's / deployed script object and replace ONLY content (+ inferred
output assets), preserving hash and metadata. + regression test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(home): redesign create-new popover and home header
Replace the home page "Home" title with a hover-driven "New" popover
(CreateActionsMenu) listing Script / Flow / Workflow-as-Code / Apps with
a description pane. Workflow-as-Code offers a Python / TypeScript choice;
other entries are created by clicking the list row. Move CLI/MCP to the
header far right, add a Hub link button, and drop the Workspace/Hub tab
switcher so the home page shows only the workspace list.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(home): keep Home title, move New to the right, swap popover panes
Reintroduce the "Home" header title on the left and place the New popover
on the right alongside the Hub and CLI/MCP buttons (top-aligned, with extra
gap before New). Swap the popover panes so the description is on the left
and the option list on the right; the menu opens leftward again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(home): make Hub, CLI/MCP and New header buttons uniform md size
Set all three header buttons to unifiedSize="md" (New keeps the accent
variant to stand out) and re-center the right group now that heights match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(home): remove divider between popover panes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(home): badge Workflow-as-Code as Advanced and low-code App as Legacy
Add inline pills (Advanced / Legacy) next to the option label and in the
description header, and widen the option list so the longest label plus
badge fits without truncating.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(home): reorder create options and share App icon
Order is now Script, Flow, App (full-code), Workflow-as-Code, App (low-code).
Full-code App reuses the low-code App dashboard icon (distinguished by accent).
Broaden Option.icon to also accept the BarsStaggered (Flow) component.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(home): add import / pipeline secondary actions to popover detail panels
Surface the previous create-menu extras in the matching detail panel:
Flow → Import flow + Pipeline (alpha); Workflow-as-Code → Import
Workflow-as-Code; App (full/low-code) → Import full/low-code app. A shared
YAML/JSON import drawer parses the pasted source into the relevant store
(or sessionStorage for the full-reload apps_raw route) and navigates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(home): add Data pipelines editor option (alpha) to create popover
Add a first-class "Data pipelines editor" entry right after Workflow-as-Code
(indigo accent, Workflow icon, emerald Alpha badge) routing to /pipeline, and
drop the now-redundant "Pipeline (alpha)" secondary action from the Flow panel.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(home): emphasize import buttons and group the badged options
Render the detail-panel import actions as default (bordered) buttons with an
import icon instead of subtle text, and add a separator in the option list
between the three plain options and the three badged (Advanced/Alpha/Legacy)
ones.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(home): increase the y gap around the option-group separator
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(home): rename Data pipelines editor option to Data pipelines
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(home): click-open create popover with import submenu and toggleable docs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(home): keyboard-navigable create popover via melt dropdown with looping
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: Guilhem Lemouel <guilhemlemouel@gmail.com>
* fix(audit): don't read pg_authid from an elevated context in S3 export migration
Migration 20260626132251 aborted instance startup on managed Postgres
(e.g. Cloud SQL) with "Modifying pg_authid or pg_auth_members is not
allowed in elevated context": the audit S3 export "oldest in-flight
xact_start" floor probe calls pg_has_role(...), which reads pg_authid,
and managed providers forbid that read from an elevated context. The
migration ran the probe inline in its UPDATE, so the whole migration —
and the instance boot — failed.
Extract the probe into a shared SQL function
audit_logs_s3_oldest_inflight_ts() that returns the oldest in-flight
xact_start (when cluster-wide stats are visible) or NULL otherwise. The
pg_has_role read is wrapped in a plpgsql BEGIN/EXCEPTION subtransaction,
so a pg_authid failure returns NULL (callers fall back to a conservative
7-day window / reject) instead of aborting. is_superuser (a GUC, no
catalog read) is checked first to short-circuit. The migration's trigger
and UPDATE, the OSS backfill try_start, and the EE exporter/startup
anchor (companion windmill-ee-private PR) all route through it.
Because 20260626132251 already shipped, it is added to the
potentially_stale list in windmill-api/src/db.rs: on startup the stale
_sqlx_migrations row (checksum mismatch) is deleted and the fixed,
idempotent migration re-applies, so already-migrated instances upgrade
without a checksum-mismatch boot failure.
Fixes WIN-2108
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 95352c13c4c82247d8cfd80936f9203aeb079802
This commit updates the EE repository reference after PR #635 was merged in windmill-ee-private.
Previous ee-repo-ref: 136f49a52af922868acac33abf8198913a9e835c
New ee-repo-ref: 95352c13c4c82247d8cfd80936f9203aeb079802
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>
The /ws_debug debugger WebSocket gated JWT signature verification on inline
`code` being present (`if (code && REQUIRE_SIGNED_REQUESTS)`), so a
`program`-mode launch (naming an arbitrary server-side file path that is read
and executed) skipped verification entirely — even with
REQUIRE_SIGNED_DEBUG_REQUESTS=true. The WS handshake also performed no Origin
check, allowing cross-origin (CSWSH) drive-by from a malicious page.
- Enforce signing on every launch in both handlers (Python + Bun/TS): reject
program-mode outright and require+verify a token for inline code.
- Add opt-in DEBUG_ALLOWED_ORIGINS allowlist enforced at the WS handshake.
- Default docker-compose REQUIRE_SIGNED_DEBUG_REQUESTS to true.
- Update THREAT_MODEL T8/EP15 to reflect the root cause and mitigation.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(apps): add labels input to app editor deploy drawer
The labels feature (c4c9ef5fd) wired LabelsInput into the script, flow,
schedule, resource and variable editors but left the app editor out: it
had no labels state and createApp/updateApp never sent labels, so apps
could not be labeled from the UI despite full backend support.
Thread the deployed app's labels from the edit page through AppEditor
into AppEditorHeader, render LabelsInput in AppEditorHeaderDeploy after
the summary field (matching ScriptBuilder/FlowSettings), and include
labels in the create/update request bodies, the savedApp snapshot, and
the diff/deploy comparison values. The raw-app editor shares the deploy
drawer, so it is wired symmetrically (createAppRaw/updateAppRaw + the
raw page loader) to avoid leaking a non-functional input there.
Fixes WIN-2107
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(apps): drop redundant labels cast in app edit restore path
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): include labels in deploy-drawer Diff current value
The Diff button inside the deploy drawer built its current value without
labels, so the approval preview could hide label changes that would be
deployed. (Identified by cubic.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): reset raw-app labels on new-draft seed
The raw-app edit route keeps labels as route-level state and the
?new_draft=true seed-template branch never cleared it. Since the route is
reused across raw-app navigations, opening a labeled raw app then creating
a fresh one could remount RawAppEditor with the previous app's labels and
deploy them via createAppRaw. Reset labels with the other bleed-prevention
resets at the top of the new-draft branch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: redeploy older app version from deployment history
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: apply restored app version to low-code editor on redeploy
Redeploying an older app version from Deployment History fired the
restore callback (toast shown) but the canvas kept displaying the
current version, and Deploy then shipped that current value.
AppEditor seeds its working state from `appDraftHandle.draft ?? app`,
preferring the per-path autosave over the freshly restored `app` prop.
The remount triggered by the restore therefore re-read the stale
pre-restore draft. `reloadDeployed` already clears the draft before
remounting for the reset-to-deployed flow; `onRestore` was missing the
same step.
Drop the autosave in `onRestore` so the remounted editor seeds from the
restored value. Raw apps are unaffected: RawAppEditor binds `files`
directly (no draft precedence), and `extractRawApp` mutates that bound
state in place.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(raw-apps): convert savedNewAppPath event forwarding to a callback prop
`svelte-check` (CI `npm check`) failed with one error: forwarding the
`savedNewAppPath` createEventDispatcher event through the runes-mode
RawAppEditor → RawAppEditorHeader chain types as "not assignable to
never". This is the same legacy-forwarding-through-runes pattern already
removed for `restore` in this PR — `on:savedNewAppPath` would likewise be
dropped at runtime, breaking navigation to the new path after a deploy
that renames the app.
Replace the `on:savedNewAppPath` forwarding with an `onSavedNewAppPath`
callback prop threaded page → RawAppEditor → RawAppEditorHeader, matching
`onRestore`. The header now invokes the callback instead of dispatching,
and its now-unused createEventDispatcher is removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: column-level lineage for ducklake pipelines via // column annotation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: auto-derive column lineage from DuckDB SQL AST (annotation as override)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: clarify column-lineage inference is server-side; drafts use annotations
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(frontend): surface inferred column lineage in live pipeline drafts
Threads the DuckDB SQL-AST column lineage (from the WASM asset parser) through
ScriptEditor -> details pane -> page -> resolveGraph, merged with // column
annotations (annotation wins) so the live preview matches the deployed graph.
Takes effect once windmill-parser-wasm-asset is republished with the inference.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(frontend): bump windmill-parser-wasm-asset to 1.740.0 for SQL column-lineage inference
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: column-lineage inference now runs live (WASM) too, merged with annotations
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(frontend): transitive column-lineage trace (impact analysis)
Stitches every producer's column_lineage into a pipeline-wide column graph
(columnLineageGraph.ts) and replaces the single-hop diagram with an
interactive ColumnLineageTrace: select an asset to see its columns' full
upstream/downstream lineage across scripts; click any column to highlight its
complete transitive impact set (forward + backward) and dim the rest.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: address CI review on column lineage (parse-fallback, node-id, perf, leak)
- backend: DuckDB SQL parse failure now falls back to `// column` annotation
lineage instead of dropping it (Codex P1)
- columnLineageGraph: collision-proof JSON node ids; deterministic first-write
output anchoring when a producer has multiple ducklake writes (cubic P2 ×2)
- pipeline page: gate buildColumnGraph to a ducklake-asset selection so it
doesn't rebuild on every editor keystroke (cubic P2)
- ScriptEditor: clear inferredColumnLineage on parse error so it can't leak
across a script switch (cubic P2)
- AssetGraphEdge: widen badge stacking offset 12px->18px to fully clear (cubic P3)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: resolve JOIN inputs + anchor column lineage to // materialize target
Addresses the second Codex review pass (two P1s):
- SQL inference now walks JOINed tables: build_from_maps maps every FROM entry
AND its joins into the alias map, and single-table attribution requires no
joins. `SELECT o.x, c.y FROM a o JOIN b c` now resolves c.y (was dropped).
- The column graph anchors a producer's lineage to its declared // materialize
target (surfaced on the runnable node) instead of guessing a ducklake
write-edge, which is unordered for deployed graphs and ambiguous for
multi-output scripts. Falls back to a write-edge when no materialize target.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: gate column-lineage badge to the // materialize target write-edge
The canvas badge keyed on `e.asset_kind === 'ducklake'`, so a multi-output
producer showed the same column mapping on every ducklake write-edge. Use the
same materialize-target anchor as buildColumnGraph: the badge lands only on the
declared output's edge, falling back to the ducklake write-edge when there's no
materialize annotation. (Codex P1)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: build column trace from displayGraph so View hides draft lineage
The transitive column trace was built from graphWithDraft regardless of mode, so
in View with drafts hidden it could surface draft `// column` lineage the
deployed canvas doesn't show. Build it from `displayGraph` (the graph the canvas
actually renders) so the trace matches: draft overlays in edit / show-drafts,
deployed-only in plain View. (Codex P2)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: don't infer column lineage for local/temp staging CTAS
A CTAS into a local/temp staging table isn't the materialized output, but its
projection was inferred and (flat) column_lineage anchored to the script's
// materialize target — so staging columns showed up as the final asset's. Gate
inference to the actual output: a top-level managed-materialize SELECT, or a
CTAS/CREATE VIEW whose target resolves to a real asset. (Codex P1)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: scope inferred column lineage to one output asset
Inference accumulated columns from every output-producing query into one flat
list, all anchored (frontend) to the script's // materialize target — so an
auxiliary CTAS into a different asset showed its columns on the materialized
one. Tag each inferred entry with its output asset and, in parse_assets, scope
the list to the // materialize target (keeping untagged top-level-SELECT
entries); with no declared target, drop inference when entries span multiple
output assets rather than attribute them to an arbitrary one. Parser-internal —
no wire change. (Codex P1)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: treat CREATE TEMP TABLE/VIEW as local even under an active USE
A one-part temp name under `USE dl` resolved to an asset (ducklake://…/tmp)
before being registered local, so a final SELECT reading it invented
`final.total <- warehouse/tmp.amt` (a phantom DuckLake column) and recorded a
phantom asset. track_table_definition now registers any temporary table/view as
local up front, bypassing active-asset resolution; CreateTable/CreateView pass
their `temporary` flag. (Codex P1)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* [ee] perf(audit): re-anchor S3 audit export on enable + opt-in backfill
The S3/GCS audit-log export's steady-state query filters by `age(xmin)`
(unindexable), so the only scan bound is the timestamp floor. On a fresh
enable the floor was epoch, and on a re-enable the cursor resumed from its
pre-disable position — either way the first run scanned the whole
`audit_partitioned` table. Under a `statement_timeout` (e.g. Aiven) that scan
never completes: the cursor never advances, nothing is exported, and the
repeated full scans saturate the database.
Re-anchor on enable (EE companion, windmill-ee-private#634):
- New trigger migration records a recent timestamp floor instead of the epoch
sentinel and `DO UPDATE`s the cursor to the current snapshot xmin on
re-enable, so the export always resumes from ~now and never rescans history.
Includes a one-time fixup for legacy epoch-sentinel checkpoints on upgrade.
Opt-in historical backfill (new `audit_logs_s3_backfill` module + endpoints):
- Exports a chosen `[from, to)` window on demand, scanning strictly by
`timestamp` (the partition key) in bounded keyset pages — each query is an
index scan capped at one page (verified via EXPLAIN: later partitions
`never executed`, ~11ms/page), so it stays well under any statement timeout
regardless of window size. Writes alongside the steady-state objects under
logs/audit/, without touching the xmin cursor.
- POST /settings/audit_logs_s3_backfill {from,to} (super-admin + Enterprise),
GET /settings/audit_logs_s3_backfill_status.
Also repurposes the status endpoint's `bootstrapping` flag to mean "draining a
backlog" (the cursor is capped and catching up), and updates the setting
description to point operators at the backfill.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(audit): heartbeat backfill lease per object; bump EE ref
Address review (cubic): persist progress (refreshing the lease heartbeat) after
every object PUT in the backfill page loop, not only once per page, so the gap
between heartbeats stays well under STALE_HEARTBEAT_SECS even on slow uploads
and another replica can't re-claim mid-page and run a concurrent backfill.
Bumps ee-repo-ref.txt to pull in the EE test-race fix (folding the backlog-drain
regression into the single audit e2e test).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(audit): reject unstable backfill windows; bump EE ref
Address review (P1): the backfill keyset-pages over rows visible at scan time
and declares completion when the scan runs dry, but a row's `timestamp` is its
inserting transaction's `xact_start`. A window whose upper bound is recent or in
the future could silently omit a transaction that started inside `[from, to)`
but commits after the scan passed that timestamp. `try_start` now rejects any
`to` newer than the oldest in-flight `xact_start` (everything strictly older
than the oldest running transaction is committed and stable), using the same
trustworthy stats gating as the exporter's floor (restricted role / 2PC → a
7-day-old cutoff).
Bumps ee-repo-ref.txt for the EE monotonic-checkpoint fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(audit): re-anchor legacy epoch checkpoints instead of synthetic floor
Address review (P1): the legacy-checkpoint fixup stamped last_oldest_inflight_ts
to now()-7d while leaving the old last_xmin in place. On an instance that
enabled export on the old code >7 days ago and got stuck before the first
successful batch, the next run would filter post-enable rows older than 7 days
out via `timestamp >= ts_floor` while still advancing last_xmin over the
interval — silently dropping them (the same floor-vs-cursor loss class fixed
elsewhere in this PR), and contradicting the "nothing committed after enabling
is skipped" guarantee.
A stuck epoch-sentinel checkpoint cannot be safely resumed (its backlog can be
arbitrarily old, so any recent floor prunes rows the cursor then skips, and an
epoch floor reintroduces the full scan). Re-anchor it to the migration's current
snapshot xmin instead — exactly like a fresh enable — so the export resumes
cleanly from ~now and the never-exported pre-upgrade window is recovered via the
opt-in backfill rather than silently dropped. Reword the setting description so
it no longer implies the disabled/legacy window is covered by the cursor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(audit): end-to-end integration tests for the object-store backfill
The backfill previously had only SQL-level/EXPLAIN validation. Add real
integration tests (in-memory object store, sqlx::test) exercising the public
path:
- backfill_exports_window_in_pages: with the page size forced to 2 rows, a
settled 3-day window is exported across multiple keyset pages; asserts every
in-window row lands exactly once, rows outside [from,to) are excluded, a day
that straddles a page boundary yields more than one object, progress counts
match, and a re-run is idempotent (deterministic keys overwritten, no dupes).
- backfill_rejects_unstable_window: a future/live `to` is rejected as unstable,
a window safely in the past is accepted.
Adds a test-only PAGE_ROWS override so multi-page behaviour is exercised with a
handful of rows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(audit): note backfill scope is audit_partitioned only
Make explicit that, like the steady-state export, the backfill reads only
audit_partitioned; the pre-partitioning `audit` table is intentionally out of
scope (not a missed case).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(audit): reject backfill windows before the partitioned boundary
Address review (Codex P1): the backfill reads only audit_partitioned, but
pre-partitioning history lives in the legacy `audit` table (still read by audit
list/get via UNION ALL, and retained for the configured period — 365 days by
default on EE). Since the setting text points operators at this API for
"pre-existing history", a window overlapping legacy rows would report completion
while silently omitting them.
Per the decision to not export the legacy table, reject instead of silently
omit: try_start now rejects a `from` earlier than the oldest audit_partitioned
timestamp (every legacy row predates the partition cutover, so a `from` at/after
that boundary can never overlap them). Reworded the setting text to scope the
backfill to the partitioned era. Added a regression test, plus an RAII guard
(cubic P2) so the test-only globals are restored even if an assertion panics.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(audit): backfill object keys per-window; require trustworthy settled cutoff
Address review (two P1s):
- Object-key overwrite loss: keys were `dt=<day>/audit_backfill_<min_id>.ndjson`.
A narrower, overlapping backfill can start a day's page at the same first row
(same min_id) but hold fewer rows, and `put` would overwrite a broader run's
object — silently dropping the rows only that object held. Include the
requested window in the key so different ranges write disjoint objects (same
window re-runs stay idempotent; consumers dedupe overlapping rows by id). New
regression test (verified red→green).
- Untrustworthy settled cutoff: when min(xact_start) isn't trustworthy (role
lacks pg_read_all_stats/superuser, or a prepared 2PC txn exists), the old
now()-7d fallback could still let an old transaction commit rows inside an
accepted window after the scan, so a "complete" backfill silently missed them.
Since a backfill asserts completeness, reject in those cases instead of
falling back. (The continuous exporter keeps its 7-day fallback — it only
claims bounded lag.)
Also makes the tests robust under the parallel runner: run_backfill takes the
store as a param, so tests pass a local in-memory store (no global
OBJECT_STORE_SETTINGS race) and serialize on the PAGE_ROWS override.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(audit): reject backfill overlapping legacy table; regen deref openapi; trim migration comment
Address review (1 P1 + 2 P2):
- Empty-partition backfill (P1): the min(audit_partitioned) guard no-ops when
audit_partitioned is empty, so an upgraded instance with legacy `audit` rows
but no partitioned rows yet would accept a window and complete with zero rows,
silently omitting the legacy rows. Check the legacy `audit` table directly:
reject any window that overlaps a legacy row (subsumes the boundary check and
covers the empty-partitioned case). Test updated accordingly.
- openapi-deref (P2): regenerate openapi-deref.yaml/json (served via include_str!)
so /openapi.{yaml,json} expose the new backfill endpoints.
- Migration comment (P2): trim the PR-history narration to the durable
constraints (why a recent floor and a monotonic cursor are required), per
AGENTS.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to b821fecccbcba2efed544890576bf2b84321d70d
This commit updates the EE repository reference after PR #634 was merged in windmill-ee-private.
Previous ee-repo-ref: 6b191b77aabcf77658ad4f9031576e0d7b66bf89
New ee-repo-ref: b821fecccbcba2efed544890576bf2b84321d70d
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat: capture managed-materialize output schema as asset metadata (#2a)
After a managed `// materialize` run, capture the producer's output schema
via a DESCRIBE folded into the existing one-row summary read (no extra
round-trip) and persist it in a new versioned `materialized_asset_schema`
sidecar table. This is the producer-side capture that pipeline parity gap
#2b (save-time consumer-ref contract enforcement) will read back.
- materialized_asset_schema sidecar (asset-level grain), versioned: a new
version row is inserted only when the captured column set changes.
- output_schema column added to the materialize summary codegen.
- worker extracts + records the schema on a successful materialize.
- /assets/asset_schemas read endpoint exposing the evolution history.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: address CI review on schema capture (partition col, order, status gate)
- exclude the synthetic `_wm_partition` column from the captured schema for
partitioned assets, so the recorded contract is the producer's logical
output, not Windmill's storage detail (claude/cubic P1).
- make the captured column list explicitly ordered (`row_number()` over the
DESCRIBE + `list(... ORDER BY)`), so the `list()` aggregate can't reorder
columns and spuriously bump the schema version (cubic P2).
- gate the API `record_materialization` schema upsert on a `Materialized`
status, so a failed/running write (or a client attaching a schema to one)
can't advance the schema history (cubic P2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: address Codex review (manual-mode schema gate + auth contract docs)
- gate output_schema extraction on the managed (`Some((Some(_), _))`) path so a
`// materialize manual` run — whose result is the user's own query output —
can't persist a caller-shaped `output_schema` into materialized_asset_schema
(Codex P2). Verified e2e: a manual run returning a fabricated
`output_schema:[{injected,EVIL}]` records the partition but writes no schema
version, while the managed path still captures normally.
- document the authorization contract on the new public `record_asset_schema`
and `list_asset_schemas` helpers: they perform no access control (mirroring
the materialized_partition siblings) and require callers to pass a
workspace-authorized executor (Codex P1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(frontend): schema-history tab on the ducklake asset node (#2a)
Adds a "Schema" tab to DucklakeAssetPanel surfacing the captured output-schema
versions persisted by the materialize run. Master-detail (mirrors the History
tab): the version list (newest first, newest auto-selected) shows column count +
snapshot + capture time; selecting a version renders its column/type table.
Reads the GET /assets/asset_schemas endpoint via raw fetch, matching the sibling
PartitionStatusGrid convention (these materialization endpoints are not in the
generated client).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: schema tab is strategy-aware (history vs fixed schema)
Only a whole-table `replace` producer (CREATE OR REPLACE) can change columns
run-to-run; `append`/`merge`/partitioned writes INSERT into a fixed-schema
table, so their schema is pinned at first materialize and the "history" framing
is degenerate (always one version).
- backend: surface the managed `materialize_strategy` (`replace`/`append`/
`merge`) on the asset-graph runnable node, alongside the existing
`partition_kind` (same parse-from-annotation path).
- frontend: the pipeline page derives `schemaCanEvolve` for the selected asset
from its write-producer (`replace` && not partitioned) and threads it to the
Schema tab. Evolvable → master-detail version history; fixed → a single
current-schema table with a short "schema is fixed" note. Unknown defaults to
evolvable so real history is never hidden.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: schemaCanEvolve fails open on unknown producer strategy
Previously a producer present but missing `materialize_strategy` (e.g. a
draft-overlay runnable, synthesized without the field) fell through to
canEvolve=false, hiding captured history behind the fixed-schema view —
contradicting the "unknown defaults to evolvable" intent.
Now the fixed view shows only when *every* producer is a known insert-style
write (append/merge, or partitioned replace); any producer with unknown
(missing) strategy is treated as evolvable, so real history is never hidden.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Global-mode chat could reference the user's existing folders in the system
prompt but had no way to create a new one, so for shared work where no
existing folder fit it would dead-end on "ask the user" or invent a
non-existent f/<folder>/… path (which fails at deploy).
- create_folder: dedicated, confirmation-gated tool for the immediate
(non-draft) folder mutation; the creator becomes an owner. Mirrors the
backend name validation client-side and returns a minimal { success } result.
- Folder path guidance now steers the model to create a folder only when the
user explicitly asks for one, and otherwise to ask which folder to use for
shared intent rather than guessing or inventing a path.
- ai_evals: in-memory create_folder mock + a create-folder case (global-path5);
path3 maxTurns bumped to give room to ask.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: skipped suspend step no longer parks the flow forever
A flow step that declares a `suspend` (approval) but is skipped via
`skip_if` was leaving the flow stuck waiting for a resume that would
never arrive.
Suspend gates the *next* step: before pushing step N, `needs_resume`
checks whether step N-1 declared a non-zero `suspend` and finished as
`Success`. A step skipped via `skip_if` is also recorded as
`FlowStatusModule::Success` (with `skipped: true`), so `needs_resume`
treated a skipped approval gate as a real one and parked the flow
waiting for an event that nothing ever sends — until the suspend
timeout (up to 24h).
The skip is most visible when the skipped suspend step is followed by a
branch/subflow: the flow appears stuck on the *following* predicate node
with a generic resume button, while none of the branch/subflow steps
ran.
Fix: honor the `skipped` flag in `needs_resume` and do not gate the next
step on a suspend that was skipped.
Adds regression test `skipped_suspend_step_does_not_block_next_step`
(times out without the fix, completes with it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: reword regression test comment as a current invariant
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(flows): reject corrupt step paths at deploy + atomic cache writes (#9751)
A flow step could execute an unrelated (and in the reported case, destructive)
script at runtime even though every stored definition looked correct. A forensic
dump traced it to two issues:
- Deploy accepted absolute/local step paths. `wmill sync push` from a feature-
branch checkout under /tmp baked an absolute path
(`/tmp/.../ops/scripts/clean_device/...`) into a step's `value.path`. Persisted
verbatim, it mis-resolved to an unrelated script at runtime.
- The on-disk cache write was neither truncating nor atomic. `FsBackedCache::put`
used `write+create`, so a shorter overwrite left stale trailing bytes and
concurrent writers could interleave into a torn file — a corrupt cached blob
that a worker then scheduled from.
Fixes:
- Reject non-workspace flow step paths (must be u/, f/, g/ or hub/) in
`validate_flow_value` (covers create_flow + update_flow, recursively through
loops/branches/AI-agent tools) and early in the CLI `pushFlow`.
- Make `FsBackedCache::put` write a unique temp file (truncate + fsync) then
atomically rename it over the target, cleaning up on error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(flows): validate failure/preprocessor module paths + sub-flow paths in CLI
Address PR review (cubic + claude):
- Backend `validate_flow_value` is the authoritative guard but only walked
`modules`; extend it to also validate `failure_module` and `preprocessor_module`
(which can themselves be sub-flows/loops/branches), so an absolute path there
can't be persisted.
- CLI preflight only collected `type: "script"` paths; now collects sub-flow
(`type: "flow"`) step paths too (recursively, incl. failure/preprocessor), so the
comment's claim matches the behavior.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): include AI-agent tool step paths in flow path preflight
Address Codex review: collectStepPaths skipped aiagent tools, so a bad path in
a tool fell through to the API error instead of the local fail-fast. The backend
already validates these (traverse_modules walks AIAgent tools); this aligns the
CLI early-error with it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(flows): make failure/preprocessor path test key explicit
The test used `slot:` as a json! key. json! does interpolate an ident key to its
variable's value (json!({slot:1}) with slot="failure_module" => {"failure_module":1}),
so the test was correct and exercised the validation — but the behavior is subtle,
so build the key explicitly via serde_json::Map to remove ambiguity (review nit).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cache): use a UUID temp name for atomic put (shared-volume safe)
Address Codex (P1): pid+counter temp names collide across container PID
namespaces on a shared cache volume (same pid, PUT_SEQ resets to 0 per process),
so two workers could truncate/clobber the same temp file before rename. Use a
random UUID suffix (matching worker.rs's atomic-write helpers) — globally unique,
so the cross-process temp-file hazard is closed. Also trims the comment to the
AGENTS.md <=4-line limit (Pi nit).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The global AI chat @-mention picker only listed flows and scripts; the
whole `app` kind was excluded, so raw (code-based) apps never appeared.
Add raw apps as a `workspace_app` reference, gated to GLOBAL mode and
filtered to `raw_app === true` so visual apps stay out.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an update_user_instructions tool to the global-mode AI chat so the
user can ask it to remember a preference or change/stop a behavior, and
it persists the change to the user-level Global custom prompt.
- update_user_instructions tool: append a new instruction, or find/replace
to edit/remove existing text (reuses the shared findAndReplace helper);
enforces the 5000-char cap and echoes current text on a failed match.
- GlobalToolHelpers gains getUserInstructions/setUserInstructions; the
manager wires them to the localStorage user-prompt store and rebuilds
the system message so the change applies on the next chat-loop iteration.
- Render workspace vs user instructions under distinct headers in the
global system prompt (getCustomPromptParts) so only the user block is
presented as editable.
- Keep the tool result lean: return a short confirmation, not the full
instructions (already re-injected into the system prompt next turn).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The GCS service account key JSON contains the secret private_key and was rendered in plain text in the settings editor on every page load (unlike S3 secret_key / Azure accessKey, which use password inputs). When a key is already configured, hide the editor behind an explicit "Show sensitive values" reveal; the editor (and thus the private_key) is only rendered on opt-in. bucket_config keeps the real key untouched while hidden, so saving round-trips correctly.
Fixes WIN-2106
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Default" node of a branch-one is built with branchIndex -1 and is the
structurally-required else branch (stored separately from the branches array),
so it cannot be removed. Its delete button still rendered, and clicking it
called deleteBranch with index 0, which in removeBranch became
branches.splice(-1, 1) — destructively removing the LAST explicit branch.
Gate the delete button on branchIndex >= 0 so it only appears on explicit
branches.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(sdk): allow overriding worker tag when running jobs
Add an optional `tag` parameter to every job-running helper across the
TypeScript, Python, PowerShell and Rust client SDKs. When set, it is
forwarded as the `tag` query param on the `jobs/run/*` endpoints, which
the backend already honors as a worker-tag override.
The parameter is appended last and defaults to null/None everywhere, so
existing positional and keyword callers are unaffected. Rust has no
optional params, so its existing `run_script_async`/`run_script_sync`
signatures are left untouched and new `*_with_tag` variants are added.
Fixes WIN-2105
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(system_prompts): regenerate SDK docs for tag param
Regenerate auto-generated system prompts so the TypeScript/Python SDK
references (and the script skills that embed them) reflect the new
optional `tag` parameter on the job-running helpers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(powershell-sdk): preserve original RunScriptAsync/RunFlowAsync arities
PowerShell class methods dispatch by exact argument count and have no
default parameter values, so adding `$Tag` in place dropped the old
4-arg `RunScriptAsync` / 3-arg `RunFlowAsync` overloads — existing direct
class calls would fail with "Cannot find an overload". Re-add the
original arities as thin overloads that forward `$null` for `$Tag`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(system_prompts): generate prompts.d.ts to stop literal-content drift
prompts.d.ts was a tracked declaration file with string-literal types
baked in, but generate.py never regenerated it — only prompts.ts and the
hand-written index.d.ts. So every prompt change (e.g. the new SDK `tag`
param) left prompts.d.ts stale, and check-freshness didn't catch it
because generate.py never wrote the file.
Emit prompts.d.ts from generate.py as plain `export declare const X:
string;` declarations. The contents now live only in prompts.ts, so the
declaration file can't drift, and check-freshness covers it going forward.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On Windmill Cloud the instance-level database is not supported for data
tables; users must point them at an external PostgreSQL resource. The
database-type picker previously labelled the "Instance" option only as
"Superadmin only", which is misleading on cloud where it can never be
enabled.
On cloud: disable the "Instance" option (subtitle "Not available on
cloud") and surface an info alert explaining that an external PostgreSQL
resource (e.g. Supabase, Neon) is required. Off-cloud behaviour is
unchanged.
Fixes WIN-2104
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removes the "Pipelines" navigation button from the assets page header
along with its now-unused NetworkIcon and base imports.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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>
* 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>
The `script run command > runs a script and returns result` test runs a
trivial, deterministic bun script and asserts exit code 0. On CI it
intermittently fails when the standalone worker (notably on Windows)
transiently fails to execute the job — identical bun jobs complete
successfully elsewhere in the same backend session, so the failure is
environmental, not a regression.
Two problems made this both flaky and undiagnosable:
- `--silent` plus asserting only on `result.code` meant the job's actual
error never reached the CI log, so a flake left no trace.
- No test-level retry, so a single transient worker hiccup failed the run.
Add `retry: 2` to the two worker-executing tests in the block, and
include stdout/stderr in the assertion label so the next occurrence is
debuggable.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
* 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>
The Claude review workflow used a plain checkout, so the EE source (the *_ee.rs
files that live in windmill-ee-private and are symlinked/gitignored in this repo)
was absent — the reviewer could only see the CE surface and missed EE-only code
like windmill-queue/src/jobs_ee.rs. Mirror the EE-checkout the Codex/Pi review
workflows already do: read the PR head's backend/ee-repo-ref.txt via the API,
check out windmill-ee-private at that ref, and substitute the EE files in (copy).
Gated on WINDMILL_EE_PRIVATE_ACCESS being present.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restoring a version from an app's Deployment History sets the editor value
directly (`onRestore`) without going through `loadApp`, so the fork base
pinned for the stale-draft check is never refreshed. The restored value
carries the `parent_version` that was baked in when that older version was
deployed, so the deploy-time guard (`compareVersions`) compares an outdated
base against the current head and falsely reports the editor is "not on
latest", surfacing a spurious override/diff confirmation on deploy.
Re-pin `parent_version` to the current head on restore, mirroring the
existing seed (loadApp) and after-deploy re-pin sites. Follow-up to #9768.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The apt-package trim in #9783 removed packages that transitively
provided libargon2.so.1. The PHP CLI binary copied from
php:8.3.30-cli-bookworm links against libargon2.so.1 (for argon2
password hashing), so PHP jobs fail at startup with:
/usr/bin/php: error while loading shared libraries: libargon2.so.1:
cannot open shared object file: No such file or directory
Explicitly install libargon2-1 so the dependency no longer relies on
an incidental transitive package.
Fixes WIN-2101
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
* 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>
* feat: detect and guard against deploying stale drafts
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: extend stale-draft warning to low-code app drafts
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor: discard stale draft on rebase instead of resetting to latest
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: animate AI chat thinking block open/close like tool calls
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: detect stale flow/app drafts by pinned version at load and deploy
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: reset version-staleness state on new drafts and after app deploy
Addresses review: new-draft route reuse left stale version/draftBaseVersion (false stale-draft modal on a fresh flow/app); app deploy left parent_version pinned to the superseded base (false 'not latest' on a follow-up deploy).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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>
* 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>
`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>
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>
* 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>
* feat: bounded-cascade selective execution for pipelines (UI + CLI)
Run a prefix of a pipeline cascade: from a schedule/manual root, fan
downstream but stop at chosen end node(s) — the path-between set over the
asset-graph lineage DAG. Exposed as a canvas 'Run downstream up to…' pick
mode and a 'wmill pipeline run <folder> --to' CLI command. No backend or
parser changes; reads the existing graph, tags, and triggers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: surface bounded-run on the run caret, trigger-node kebab, and Test button
Move 'Run downstream up to…' from the runnable kebab onto the play-button
caret popover (Edit mode, next to Run / Run + trigger N downstream); add it
to the trigger-node kebab so schedule/data_upload entrypoints expose it on
the View page; and to the ScriptEditor Test split caret for the open script.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: address CI review on bounded-cascade (cubic)
- Port CLI engine test from Deno to bun:test under cli/test/ (won't run under bun test otherwise).
- closure() now excludes the start node on a cycle back to it (descendants/ancestors contract); regression tests both engines.
- CLI 'pipeline run --to' rejects unresolved/ambiguous end tokens instead of silently running a different subset.
- Sort a copy in the runSelection order test so the launch-order assertions aren't invalidated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: address standing review nits on bounded-cascade
Resolves the four recurring P1/P2 findings from the codex/pi/claude
reviews:
- UI gate (P1): the canvas/trigger-node "Run downstream up to…"
affordance was gated on the subscriber-only downstream map, so a valid
start whose only downstream is a pure reader had a non-empty bounded
set but no menu entry. Gate on the read-aware lineage downstream
(buildLineageDownstreamMap), matching the bounded engine.
- waitJob (CLI): a completed job without explicit success:true now
counts as a failure, mirroring the frontend waitJobTerminal — the
cascade only advances on a confirmed success.
- Comment fix (CLI): the unbounded `run` path uses the read-aware
lineage DAG (pure readers included); dropped the false "parity with
the canvas cascade" (subscriber-only) claim.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: expose bounded-run caret for pure-reader-only starts (codex P1)
The canvas wiring from the prior commit passed `onStartBoundedRun` from
the read-aware lineage map, but the leaf components still hid the popover
that holds the "Run downstream up to…" action behind a subscriber-only
gate:
- RunnableNode rendered the Run-button caret only when
`hasCascade = downstreamCount > 0` (subscriber-only). A valid start
whose only downstream is a pure reader got `onStartBoundedRun` but no
visible action. Now the caret opens when there's a cascade OR a
bounded-run start (`hasCaret`), and the "Run + trigger N downstream"
item is gated on `hasCascade` so it never reads "trigger 0".
- ScriptEditor's Test split button activated only when
`downstreamSubscribers > 0`, falling through to a plain Test button
(no caret) otherwise. Now it also activates when `onBoundedRun` is
set, with the "Test + trigger N" item gated on the count.
For a manual root (no trigger-node kebab fallback) with a pure-reader
downstream this was the only UI entry point, so it was previously
unreachable. Verified in-browser: a manual-root script writing an asset
read-only downstream now exposes "Run downstream up to…" on the
ScriptEditor Test caret with the cascade item hidden.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: gate ScriptEditor bounded-run on read-aware downstream; fix CLI asset-end warning (codex P2)
- Details-pane (ScriptEditor) bounded-run entry was gated only on
`validStartPaths`, broader than the canvas which also requires
read-aware downstream (`hasLineageDownstream`). An isolated start could
thus expose "Run downstream up to…" and enter pick mode with no
selectable end. Now gated on `lineageDownstreamPaths` (script paths with
a downstream in `buildLineageDownstreamMap`), matching the canvas.
- CLI dropped-end warning called `scriptPathOf(d)` unconditionally, which
slices `script:`-length chars off an asset id too — `datatable:main/raw`
printed as `le:main/raw`. Now prefix-checks like the JSON output.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: correct --from error to exclude only row-backed event triggers (codex P2)
The bounded-start validation message listed `kafka/webhook/…` as event
triggers that can't start a bounded run, but webhook/data_upload are
rowless and read as manual roots (valid starts). Only the row-backed
native kinds (kafka/mqtt/nats/postgres/sqs/gcp/email — EVENT_TRIGGER_KINDS)
are excluded; the message now names those.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: surface dropped ends in CLI JSON; disambiguate shared-trigger bounded start (codex P2)
- CLI `run --json` silenced the dropped-end warning, and the JSON payload
echoed the originally-resolved `--to` list with no reachable/dropped
split — a resolved-but-unreachable end looked like a clean plan that
silently runs only the start. JSON now includes `reachableEnds` and
`droppedEnds` (shared `idLabel` helper, asset-id safe).
- Trigger nodes dedupe per (kind, ref), so a schedule shared across
scripts collapses to one node, but `recordSourceTrigger` kept only the
first target path — the bounded-run action then rooted at an arbitrary
script (or hid when only that first script lacked downstream). Now all
target paths are tracked and the action is offered only when exactly one
is a valid start with downstream; multi-eligible nodes suppress it
rather than guess.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: don't run hidden drafts in View-mode bounded cascade (codex P1)
launchCascadeScript unconditionally preferred drafts.get(path) over the
deployed script. In View mode with drafts hidden (displayGraph is
deployed-only), a bounded run started from a trigger-node kebab would
execute preview jobs from hidden local draft content instead of the
deployed scripts the user is looking at.
Gate draft execution on `mode === 'edit' || includeDrafts` — the exact
condition under which displayGraph includes drafts — so execution always
matches the displayed graph. No-op for scripts without a draft; the
edit-mode "Run + trigger N downstream" cascade is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`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>
* 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>
* fix(frontend): nested-loop "Test this step" resolves iter to innermost loop
In a loop-inside-a-loop, the inner step's "Test this step" tab prefilled
its arguments using the outermost ancestor as the parent module, so
flow_input.iter resolved to the parent loop's iteration value instead of
the inner loop's.
dfs(id, flow, true) returns [step, immediate parent, ..., root], so
modules[modules.length - 1] is the outermost ancestor. The prop picker
needs the immediate parent (modules[1]) so getFlowInput resolves iter at
the innermost loop's level. A single loop was unaffected because both
indices coincide; only depth >= 2 broke.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(frontend): nested-loop parent selection for test-step args
Pins that modules[1] from dfs(stepId, flow, true) is the immediate parent
for every step across all container types (for/while loops, branchone,
branchall, aiagent tools) and nesting depths, and that getStepPropPicker
then resolves flow_input.iter to the innermost enclosing loop.
Covers >400 step positions across 107 generated flow shapes, plus explicit
single/nested/while/branch iter-resolution cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(frontend): remove nested-loop parent selection test
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: 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>
* 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>
* feat(ai-chat): add /clear session command to start a fresh conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): don't re-queue a built-in command flushed from the queue
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add two agent skills under .agents/skills (symlinked into .claude/skills):
- ai-chat: guidance for improving the Windmill AI chat / copilot, especially
global mode — benchmark before/after with ai_evals, optimize finalContextTokens
over cumulative, keep tool params and tool-result payloads minimal (no echoing
content the model already has), treat prompts/tool-descriptions as benchmarkable
surface.
- ai-evals: author and run black-box benchmark cases for the AI generation modes,
migrated from ai_evals/AGENTS.md and extended with run mechanics (workspace reuse,
reading the summary). ai_evals/AGENTS.md becomes a pointer stub to the skill.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): persist on-behalf-of user when redeploying raw apps
The raw-app deploy drawer reused AppEditorHeaderDeploy but never wired up
the `preserveOnBehalfOf` bindable nor forwarded `preserve_on_behalf_of` in
the createAppRaw/updateAppRaw request bodies. Without that flag, the shared
backend handler (create_app_internal/update_app_internal) resets the policy's
on_behalf_of to the deploying user on every deploy. So a publisher who set
"App executed on behalf of <other user>" would silently lose it on the next
deploy, unlike every other setting on the deploy page.
Mirror the classic (low-code) app header: declare `preserveOnBehalfOf`, bind
it to the deploy component, and send `preserve_on_behalf_of` on both create
and update.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): preserve on-behalf-of in the draft-deploy path
The draft-deploy path (deployDraft → AppService.createApp/updateApp for visual
apps, deployRawAppDraft → createAppRaw/updateAppRaw for raw apps) carries the
deployed app's policy forward but never sent preserve_on_behalf_of. So
deploying a draft via the "Review & deploy drafts" UI silently reset the
policy's on_behalf_of to the deploying user — the same backend reset behind the
deploy-drawer bug, on a surface that has no on-behalf-of selector to re-set it.
Send preserve_on_behalf_of whenever the carried policy has an on_behalf_of, for
both app types. The backend still gates actual preservation on
can_preserve_on_behalf_of, so a non-deployer cannot escalate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): preserve on-behalf-of in the AI-chat raw-app deploy
The global AI-chat deploy path (`deploy_workspace_item` → createAppRaw/
updateAppRaw in copilot/chat/global/core.ts) carried the recomputed policy
forward but omitted preserve_on_behalf_of, so deploying a raw app via chat
reset the policy's on_behalf_of to the deploying user — the last of the
deploy surfaces with this gap. Send the flag when the policy has an
on_behalf_of, mirroring the editor and draft-deploy paths; the backend still
gates preservation on can_preserve_on_behalf_of.
Add a regression test asserting the flag is forwarded when the deployed
policy carries an on_behalf_of.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(frontend): restore raw app 'open preview in separate window'
Re-adds the detached preview window dropped when preview hosting moved to
the host (ui-builder f52d8e5b). Live-synced preview + dark mode, and wires
the runnable bridge to the detached window so backend calls resolve there.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): repaint detached raw app preview after refresh
The detached preview tab is a blank app-preview.html shell fed by the
editor over postMessage. A one-shot opener load listener can't survive the
tab refreshing itself, so a manual reload left it blank. It now posts
'appPreviewReady' on every (re)load and the editor re-sends the build; the
orphaned window is also closed when the editor unmounts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): keep load-based feed for detached preview initial open
Relying solely on the appPreviewReady handshake left the detached window
blank on first open against app-preview.html artifacts that predate the
handshake (the pinned UI Builder tarball). Restore the one-shot load feed so
initial open works regardless of the served shell; the handshake still
covers manual refresh.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): re-sync dark mode when refocusing detached preview
The focus-reuse path replayed the build but not the theme, so re-opening an
existing detached window after a dark-mode toggle kept the stale theme until
the next build. Extract a feedExternalPreview() helper (theme + build) used by
the open, focus-reuse, load and handshake paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(frontend): bump ui_builder artifact to 062d11c (preview handshake)
Pins the UI Builder artifact built from windmill-code-ui-builder#14, which
adds the appPreviewReady handshake, detached-preview favicon and title.
Activates refresh-repaint + favicon for the detached raw app preview.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(frontend): serve ui_builder static bundle in dev, proxy :4000 only as fallback
The postinstall downloads the pinned UI Builder artifact into static/ui_builder,
which SvelteKit already serves at /ui_builder. Skip the :4000 proxy when that
bundle is present so dev uses it directly (matching prod / the backend's
static-vs-:4000 fallback); no separate UI Builder dev server needed. Delete
static/ui_builder to develop the builder against :4000.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): harden detached preview origin + scope window name
Addresses cubic review on #9765:
- P1: only honor appPreviewReady from a same-origin sender and post the build
with targetOrigin=location.origin, so user app code that navigates the
detached window cross-origin can't trigger/receive a build (app source).
- P2: scope the detached window name per app path so two open editors don't
collide over one OS-level preview window.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): show context usage as a gauge with hover tooltip
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): consolidate model, thinking & params into one dropdown
Merge the model picker, reasoning-effort selector and prompt settings
into a single dropdown with a model list, a thinking-effort slider and a
hover-revealed Parameters submenu. The trigger shows the model and effort.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): polish model/thinking dropdown interactions
Register the model rows and thinking slider as melt menu items (roving
highlight + arrow-key navigation), keep the menu open on selection via a
new DropdownV2 closeOnItemClick prop, use melt's createSubmenu for the
Parameters flyout so it flips on screen edges, and use the brand accent
for the context-usage gauge and slider.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): stop popover drift and keep Thinking section when unsupported
Freeze the trigger width while the dropdown is open so the bottom-end
popover doesn't shift as the effort label resizes (released on close, so
no reserved padding). When a model has no reasoning support, show the
Thinking section disabled with a message instead of removing it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): restore reasoning slider drag inside the menu
The slider lives in a melt menu item, whose roving focus blurs the
focused element on pointermove and aborted the native thumb drag. Stop
the slider's pointer events from bubbling to the item so melt leaves it
alone; focus-based highlighting still works.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): move Parameters to the top of the model settings menu
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-chat): hide the @ context picker in global mode
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): only mark context gauge as a meter when the window is known
A meter is a 0–100% reading; with an unknown context window there is no max
to measure against, so role/aria-value* are dropped (previously valuenow fell
back to the raw token count against an implicit valuemax of 100).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(frontend): note closeOnItemClick is read at mount-time
Addresses a non-blocking review note on DropdownV2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(ai-chat): fix showContextPicker comment to match GLOBAL removal
Addresses Pi review P2: GLOBAL no longer offers the @ context picker.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(ai-chat): clarify showContextPicker hides only the manual @ button
In GLOBAL, @-context is still invoked inline by typing @ in the input; only
the redundant picker button is hidden.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): stop flow step id generation from being poisoned by non-canonical keys
nextId computed the next step id from the max of charsToNumber over every
module id and flowState key. Only canonical auto-ids (a, b, ... aa, ab) have a
meaningful charsToNumber value, but flowState also holds copy ids ("z2"),
subflow result keys ("subflow:..."), reserved keys ("failure"/"preprocessor")
and user-renamed ids. The old `length >= 4` guard filtered long junk but let
short junk through, so e.g. duplicating step "z" (key "z2", charsToNumber 629)
made the next new step jump to "xg" and escalate from there.
nextId now only counts a key if it round-trips through numberToChars and is not
reserved, and the broken length cap is removed so large flows still get correct
ids.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): keep length cap in nextId to avoid regressing long renames
Address CI review: removing the length cap made all-lowercase renamed step
ids (e.g. "process", which round-trips through numberToChars) feed into the
max and poison id generation again — a regression versus the prior behavior,
since step ids can be renamed to ^[a-zA-Z][a-zA-Z0-9_]*$.
Restore the length>=4 skip and pair it with the round-trip canonical check,
so short non-canonical keys (copy ids "z2"/"c10", reserved/renamed short ids)
no longer poison the max while long renames stay out of the sequence. Update
the tests to reflect the actual 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>
* 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>
* 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>
* 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>
The global fork modal (and other modals) portal into `#content`, but that
element only existed in AiChatLayout's `!disableAi` branch. On the AI-session
route `disableAi` is true, so the `{:else}` branch rendered without `#content`,
and opening the fork modal there threw "No element found matching css selector:
#content". Give the else-branch container the same `id` so the portal target is
always present in this layout.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
* 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>
* feat(apps): show raw-app fork diffs as per-file tree items
Raw-app diffs previously rendered as one big YAML diff of the whole
serialized app. This explodes a raw app into separate, independently
collapsible diff items — one per file, one per runnable, and an
app.yaml metadata item — that flow through the existing fork-diff
list, sidebar tree, search and count via composite paths
(<appPath>/<file>). Runnables render as script/flow rows (code shown
in a Content tab), and files get extension-specific icons reused from
the raw-app editor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: remove raw-app tree-diff plan doc from the branch
The implementation plan was an authoring aid, not product documentation; drop it so it doesn't ship in the PR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: present raw app as an app-headed folder in the diff tree
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: narrow RawAppFileItem in diff viewer branch (fixes svelte-check)
DiffRow.kind is a plain string so the kind check didn't narrow the union; assert the synthetic item. Also size-guard on the larger side's line count instead of the doubled total, and document normalizeRawApp's per-field value-wrapper precedence.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style: single-line, lighter diff-tree rows for all item kinds
Add a singleLine mode to WorkspaceItemRow (summary ?? path on one line; DRY'd via a shared body snippet) and use it for every diff-tree leaf, so scripts/flows/triggers/resources/etc. match the raw-app header. Bump rows to py-1.5, force font-normal, and split colours: items in text-primary, folders in text-secondary.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor: extract pure diffTree model from WorkspaceDiffDrawer
Move tree construction + keyboard-nav traversal + the folder-keying convention out of the 775-line component into a pure, generic, tested module (buildDiffTree → root/order/parentKeyOf/firstChildKeyOf). Parent and first-child come from a child→parent map built during construction, not from re-splitting a path at the call site, so a node's tree position and its nav parent can't drift — the class of bug behind the ArrowLeft regression. Deletes the forkDiffNav half-seam (its bug lived in the untested caller). 12 new unit tests cover order/parent/first-child incl. the storage-key-vs-friendly-path case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): keep raw-app metadata flag + dedup runnables across path collisions
Addresses two P2 review nits (Codex/claude): (1) rawAppDiffToItems marked metadata by matching path==='app.yaml', so when a real file is named app.yaml the reserved app.yaml~2 metadata item lost its flag/full-YAML toggle — now parseRawAppDiff tags the entry with isMetadata and the items read the flag; (2) runnable composite leaves weren't deduped against real files, so a real file at runnables/<name> could produce a duplicate leaf — now reserved (slash-normalized) like parseRawAppDiff. +2 tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): dedup /app.yaml metadata collision + disambiguate synthetic row keys
Two follow-up P2s from Pi/Codex re-review of the prior fix: (1) parseRawAppDiff's collision set used raw file keys, so a real file /app.yaml (leading slash, which joinAppPath strips) still collided with the synthetic app.yaml leaf — now slash-normalized via a shared stripLeadingSlash, +test. (2) synthetic raw-app items (runnables rendered as script/flow) could share kind+path identity with a real workspace script/flow at <appPath>/runnables/<name>, causing duplicate {#each} keys and broken nav — itemKey now prefixes synthetic items (rawapp:).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(apps): canonicalize raw-app file keys to dedup leading-slash collisions
Codex P2: a file keyed /App.tsx on one side and App.tsx on the other became two entries that joinAppPath collapsed to one composite path → duplicate row key. asFileMap now strips the leading slash so both sides resolve to one file. +test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(apps): lazy-mount per-file diff editors as they scroll into view
Exploding a raw app into N per-file rows mounted N Monaco DiffEditors at once (3 reviews flagged it). Each block's editor now mounts only when it scrolls within ~200px of the viewport (IntersectionObserver rooted on the scroll container), showing a light placeholder until then; mountedRows latches so it never unmounts on scroll-away. Verified: ~6 of 13 mount initially, the rest on scroll.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AI skills are only consumed by the GLOBAL chat mode's system prompt, and
global mode itself is dev-gated by isGlobalAiEnabled(). Gate the workspace
AI skills settings tab on the same flag so it isn't shown when the skills
can't be used, and add it to gate.ts's rip-out inventory.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai-chat): strip unclosed <summary> tag leaking into compaction summary
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ai-chat): strip analysis before matching summary to avoid scratchpad leak
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: 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>
* feat: render mermaid diagrams in chat code blocks
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: guard mermaid render against out-of-order async and transient streaming failures
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: only show mermaid diagram while it matches current source
Addresses Codex review: keeping the last good SVG through parse failures left a stale, mismatched diagram on screen when the source changed to something invalid. Tie the rendered SVG to the source that produced it and only display it while it still matches the current code, falling back to the raw source otherwise.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(telemetry): disclose resource and infra usage stats
When minimal telemetry is disabled, the stats payload now includes resource
counts (workspaces, scripts per language, flows, workflows as code, low-code
and raw apps) and, on EE only, infrastructure info (container runtime,
database size, max connections, RDS detection).
Update the telemetry disclosure in instance settings accordingly: resource
counts are listed for both CE and EE; infra info is shown only on EE since it
is collected only there. Bump the EE ref and add the sqlx cache for the new
queries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(telemetry): expand EE infra disclosure and add sysinfo dep
Disclose the expanded EE infrastructure telemetry (deployment mode, host
OS/arch/CPU/memory, filesystem space, Postgres version and connection counts,
object storage backend, sandboxing and retention settings) in instance
settings. Add sysinfo as a windmill-common dependency for host memory and
filesystem stats, bump the EE ref, and add the sqlx cache for the new queries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(telemetry): focus EE infra disclosure on wrapping platform
Drop the single-server host details (OS, arch, CPU, memory, filesystem) and
tuning config from the EE infra disclosure, and revert the sysinfo dependency
they required. Reflect managed-database-provider detection in place of the RDS
flag. Bump the EE ref and update the sqlx cache for the revised queries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(telemetry): drop deployment mode and worker count from disclosure
Remove deployment mode and worker count from the EE infra disclosure to match
the backend, and bump the EE ref. They reflect only the node sending telemetry,
not the deployment topology.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 6d3301507db50818f1683dac3941d3e0cf1152a7
This commit updates the EE repository reference after PR #627 was merged in windmill-ee-private.
Previous ee-repo-ref: d30e7d18d14992598a97356d0ed13f7d5d585115
New ee-repo-ref: 6d3301507db50818f1683dac3941d3e0cf1152a7
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
* fix: allow non-super-admin object storage test, harden SSRF surface on cloud
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: validate effective object storage host to close region/bucket SSRF bypass
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: validate gcs_base_url/token_uri in GCS service account key to close SSRF bypass
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: match url scheme case-insensitively in object storage host validation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The expired-job retention loop re-scanned the same oldest rows on every batch. When
the oldest completed jobs are undeletable (children of a still-active root flow), the
ORDER BY completed_at ASC scan walked that protected prefix on each of the up-to-20
batches, doing a v2_job PK lookup per row — quadratic in prefix size (measured ~9s/batch,
~180s/cleanup-cycle on a 1.5M-row prefix).
Carry a completed_at watermark (max deleted) across batches and re-apply it as
completed_at >= floor so each batch resumes past the already-processed prefix. Also skip
the v2_job join entirely when no old root flow is active (the common case), since nothing
is protected then. Measured: subsequent batches 9000ms -> 159ms; empty-set path 154 -> 36ms.
The watermark only ever skips rows the current run already deleted, was protecting, or
skip-locked — all deferred to the next run, identical to the unbounded scan's row set
(verified: union of batched deletes == single delete, 0 diff). Mirrored in
windmill-api-settings log_cleanup.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(frontend): debounce external code→Monaco sync in Editor
Make the external `code` prop → Monaco sync always-on and 500ms
debounced, replacing the opt-in `syncExternalCode` prop. Removes the
prop from the two inline rawscript call sites in FlowModuleComponent.
Includes temporary debug scaffolding (A→B executeEdits button and
console logs) for diagnosing successive-edit behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(frontend): share alignCodeWithEditor + bump debounce to 800ms
Extract the full-range executeEdits sync into alignCodeWithEditor() and
reuse it from both setCode and the debounced external-code effect. Bump
the external-sync debounce 500ms -> 800ms. ScriptEditor now calls
editor.setCode when syncing external code in.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* nits
* nits
* Fix AI not seeing latest code
* remvoe debug button
* nit types
* nits
* Nits
* Check timeoutModel is undefined
* fix(frontend): suppress editor echo in external code sync to prevent typing clobber
* fix(frontend): cancel pending keystroke debounce in setCode to prevent clobber
* fix(frontend): preserve pending external code write in updateCode
* Revert "fix(frontend): preserve pending external code write in updateCode"
This reverts commit 731d877730.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Diego Imbert <diego@windmill.dev>
Closing the Instance settings drawer cleared the underlying script
editor. On unmount, SuperadminSettingsInner.removeHash() stripped the
`#superadmin-settings` hash with a SvelteKit `goto()`, and that
navigation re-fired the script editor page's path-reactive `$effect`,
reloading the script and wiping unsaved editor content.
Use `replaceState` to drop the hash without a navigation (matching the
existing RunForm.svelte pattern), guarded against router-teardown throws.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: reject pipeline `# tag` annotation false-positives on regular comments
`parse_pipeline_annotations` treats any comment line starting with
`# tag <text>` as a worker-tag annotation. In Python scripts, ordinary
English comments beginning with "# tag ..." were misinterpreted: values
over 50 chars failed the `script.tag` INSERT (varchar(50)), and shorter
ones silently overrode the script's worker tag.
Worker tags are single-word identifiers (e.g. `heavy`, `gpu`), so reject
any candidate that contains whitespace or exceeds 50 characters. Mirror
the same validation in the TS parity parser and add regression tests on
both sides.
Fixes WIN-2090
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: restrict pipeline annotation scan to the leading comment header
The root cause of the `# tag` false-positive is broader than the `tag`
keyword: `parse_pipeline_annotations` scanned every comment line in the
whole file, so any body comment matching an annotation grammar
(`on`, `freshness`, `tag`, `retry`, ...) was misinterpreted. The `tag`
case was the most visible because an over-length value crashed the
`script.tag` INSERT (varchar(50)).
Windmill's other comment-directive parsers (BashAnnotations::sandbox_image,
ssh_target) already scan only the leading comment header and stop at the
first line of real code. Align parse_pipeline_annotations (and its TS
mirror) with that convention: skip blank lines, break on the first
non-comment line. This eliminates body-comment false-positives for every
annotation, not just `tag`.
The `tag` whitespace/length guard from the previous commit is kept as
defense for prose that sits in the header itself.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:35:37 +00:00
1206 changed files with 105747 additions and 16393 deletions
- Indexes on foreign keys + any frequently-filtered columns
- Foreign key to `workspace`
- The RLS policies (`see_own`, `see_member`, `see_folder_extra_perms_user_*`, `see_extra_perms_user_*`, `see_extra_perms_groups_*`), copied from an existing trigger table
**RLS: wrap every session GUC read in a scalar sub-select.** Write the session
reads as `(select current_setting('session.user'))`,
description:Guidance for improving the Windmill AI chat (copilot), especially global mode — tools, prompts, and context-window discipline. Use when editing chat tools, system prompts, or tool-result shapes under frontend/src/lib/components/copilot/chat, or when changing how the chat manages its context window.
---
## Always benchmark before and after
No context or behavior change ships without an `ai_evals` A/B on the affected mode.
Add or adjust cases for exactly what you changed — see the `ai-evals` skill for
authoring and the full run reference.
Run the affected mode **before** your change and **after**, same model(s), same cases.
## Measure the window first, and cumulative second
Optimize **`finalContextTokens`** (window occupancy — what drives overflow and
compaction), then cumulative prompt tokens.
## Context discipline
The dominant fixed cost is per-iteration overhead: the system prompt **plus every
tool schema** is re-sent on every loop iteration. So:
- **Every tool and every parameter is a permanent tax.** Justify each one and measure
it; an extra "locate" round-trip can cost more than the reads it saves. Strip dead
params rather than leaving them in the schema.
- **Tool results return the minimum.** Never echo content the model already has. The
canonical mistake: a write tool that returns the whole edited artifact right after
the model authored it — return `{ success, message }` instead. When you touch a
*shared* write helper (e.g. `finishAppDraftWrite` in `global/core.ts`), re-check
this invariant for **all** the write tools routing through it — the echo has
regressed before via a shared refactor.
## Prompts and tool descriptions are part of the surface
The system prompt and tool descriptions steer behavior as much as the tools
themselves, and are benchmarkable the same way. A description that advertises
truncation makes the model self-limit; the path-conventions block changes where
drafts land. Treat prompt/description edits as real changes and A/B them — a
pure-prompt change is a legitimate, measurable improvement.
description:Author and run black-box benchmark cases for the Windmill AI generation modes (flow/app/script/cli/global) in ai_evals/. Use when adding or changing eval cases, or when running before/after benchmarks for AI chat / copilot changes.
---
# AI evals — authoring and running benchmark cases
`ai_evals/` is a black-box benchmark runner for the Windmill AI generation modes:
`flow`, `app`, `script`, `cli`, `global`. It always tests the **current** production
prompts, tools, and guidance in this checkout. Each attempt runs the real production
path, deterministic validation, then LLM judging.
The goal is to test current production guidance with realistic user requests — **not**
to pin one exact implementation shape.
## Running benchmarks
```bash
cd ai_evals
bun install # first time; frontend modes also need `cd frontend && bun install`
bun run cli -- models # list model aliases
bun run cli -- cases global # list cases for a mode
bun run cli -- run global global-test1-script-create --model sonnet
```
Frontend modes (`flow`/`script`/`app`/`global`) route model calls through a Windmill
backend's `/api/w/<ws>/ai/proxy`, so you need **any** reachable backend:
description:Run the CI Codex PR review locally against this branch's unpushed work (committed + uncommitted) before pushing. Same policy, model, and reasoning effort as the codex-pr-review GitHub action.
---
# Local Codex Review (pre-push)
Runs the exact same review Codex performs in CI (`.github/workflows/codex-pr-review.yml`),
but locally and scoped to work you have not pushed yet — so you catch what CI would flag
before the PR exists. Use this before `git push` on a non-trivial change.
- Output: markdown starting with `## Codex Review`, findings tagged P0 / P1 / P2 with file:line.
**Differences from CI** — local-only:
- Scope is the current branch vs `main` at the merge-base, **including uncommitted changes** (CI reviews a pushed PR diff).
- Sandbox is `read-only` (CI uses `danger-full-access` on an ephemeral runner). Codex reads the diff and files but cannot modify your working tree.
- Fresh context is inherent: `codex exec` is a separate cold process, so it does not anchor on the current chat session — the same reason `local-review` insists on a subagent.
## Prerequisites
-`codex` CLI **>= 0.144.1** installed and authed (`codex login` or `OPENAI_API_KEY`). Older CLIs reject `gpt-5.6-sol` with "requires a newer version of Codex". Upgrade with `npm install --global @openai/codex@0.144.1` (may need `sudo` for a global install). Keep this in sync with the pin in `.github/workflows/codex-pr-review.yml`.
-`git fetch` the base ref if it's stale, so the merge-base is accurate.
## Run
```bash
bash .agents/skills/local-review-codex/run.sh # review vs main (default)
bash .agents/skills/local-review-codex/run.sh <base> # review vs a different base ref
```
Invoke with `bash` (or run the executable directly) — the script needs Bash for
`set -o pipefail`; `sh` is Dash on Debian/Ubuntu and would fail. If `main` isn't a
local branch (e.g. a fresh single-branch checkout), the runner falls back to
`origin/main` automatically.
The script computes `BASE_SHA = git merge-base HEAD <base>`, feeds Codex `REVIEW.md` plus a
diff context pointing at `git diff <BASE_SHA>` (which folds in uncommitted edits), and prints
the review. It writes only temp files — nothing lands in the working tree.
## Relaying the result
Print the Codex output verbatim. Do not re-summarize or filter it — the value of a cold Codex
pass is surfacing what the current session would rationalize away. Then decide with the user
whether to address findings before pushing.
For a Claude-native review instead, use `local-review` (branch-diff-reviewer subagent). This
skill is the Codex counterpart; run both for independent perspectives.
echo"No changes vs $BASE_REF — nothing to review." >&2
exit0
fi
PROMPT="$(mktemp)"
OUT="$(mktemp)"
trap'rm -f "$PROMPT" "$OUT"' EXIT
# REVIEW.md is the shared policy CI feeds Codex. Append the local output-format
# and diff context inline (CI reads these from a generated context file; inlining
# keeps the working tree clean — no scratch files land in the repo).
cat REVIEW.md > "$PROMPT"
cat >> "$PROMPT"<<EOF
# Codex output format
- This is a pre-push LOCAL review of unpushed work; there is no PR yet.
- Inspect the changes by running the diff commands in the review context below.
- Untracked files do NOT appear in \`git diff\`. Review every untracked path listed below by reading it directly (\`cat\`) — treat its entire contents as newly added.
- Return markdown starting with \`## Codex Review\`.
- Tag each finding with a severity (P0 / P1 / P2), file path, and line number when known confidently.
# Review context
Local review (pre-push): current branch vs $BASE_REF
Base SHA: $BASE_SHA
Head SHA: $HEAD_SHA (plus any uncommitted working-tree changes)
Changed commits command:
git log --oneline $BASE_SHA..HEAD
Changed files command:
git diff --stat $BASE_SHA
Full review diff command (tracked changes, includes uncommitted edits):
git diff --unified=0 $BASE_SHA
Untracked files (NOT in the diff above — read each one directly, it is entirely new):
@@ -96,7 +96,11 @@ and continue once they confirm it's done.
1. Run `git status` to check for uncommitted changes
2. Run `git log main..HEAD --oneline` to see all commits in this branch
3. Run `git diff main...HEAD` to see the full diff against main
4. **Invoke the `local-review` skill** before creating the PR (`/local-review` in Claude Code, `$local-review` in Codex, `pi --skill local-review` / `/skill:local-review` in Pi). If issues are found, fix them and commit before proceeding. Do not skip this step.
4. **Review the diff before creating the PR — run both reviews, do not skip:**
- **`local-review`** — Claude-native branch-diff-reviewer (`/local-review` in Claude Code, `$local-review` in Codex, `pi --skill local-review` / `/skill:local-review` in Pi).
- **`local-review-codex`** — cold Codex pass, the same review CI runs, for an independent perspective the Claude pass misses (`/local-review-codex` in Claude Code, or `bash .agents/skills/local-review-codex/run.sh`). If the `codex` CLI is missing or older than the version pinned in that skill, note it in your summary and continue — never block the PR on codex being unavailable.
Run both — they catch different things. If either surfaces issues, fix them and commit before proceeding.
5. **Screenshots for frontend changes**: if `git diff main...HEAD --name-only` matches `^frontend/`, capture and embed screenshots of the affected UI per "Screenshots" above before writing the PR body (skip only if there is no visible UI effect).
6. Check if remote branch exists and is up to date:
- Read `./.github/pi/pr-review-context.md` for PR metadata and the diff commands.
- Read the reviewcontext file whose absolute path is given at the end of these instructions; it holds the PR metadata and the diff (or the git commands to produce it).
- Return a markdown PR comment starting with `## Pi Review`.
- Tag each finding with a severity (P0 / P1 / P2), file path, and line number when known confidently.
- Output ONLY the final review markdown — no preamble, no thinking, no tool transcripts.
@@ -16,7 +16,7 @@ Open-source platform for internal tools, workflows, API integrations, background
- **Backend patterns**: use the `rust-backend` skill when writing Rust code
- **Frontend patterns**: use the `svelte-frontend` skill when writing Svelte code. Do NOT edit svelte files unless you have read that skill.
- **Frontend UUIDs**: do not call `crypto.randomUUID()` in frontend code. Import `randomUUID` from `$lib/utils/uuid` instead.
- **Code review**: review the current PR or branch against the shared review policy in `REVIEW.md` (severity triage, public-surface checklist, AGENTS.md compliance, test-coverage assessment). The skill at `.agents/skills/local-review/SKILL.md` orchestrates it. All three CLIs auto-discover the same SKILL — Claude reads `.claude/skills/` (symlinked to the canonical `.agents/skills/` file), Codex and Pi read `.agents/skills/` directly. Invoke with `/local-review` in Claude Code, `$local-review` (or `/skills` selector) in Codex, or `pi --skill local-review` / `/skill:local-review` in Pi.
- **Code review**: review the current PR or branch against the shared review policy in `REVIEW.md` (severity triage, public-surface checklist, AGENTS.md compliance, test-coverage assessment). The skill at `.agents/skills/local-review/SKILL.md` orchestrates it. All three CLIs auto-discover the same SKILL — Claude reads `.claude/skills/` (symlinked to the canonical `.agents/skills/` file), Codex and Pi read `.agents/skills/` directly. Invoke with `/local-review` in Claude Code, `$local-review` (or `/skills` selector) in Codex, or `pi --skill local-review` / `/skill:local-review` in Pi. For a Codex-driven pass that mirrors the `codex-pr-review` GitHub action against your unpushed work (committed + uncommitted) before you push, use `/local-review-codex` (`.agents/skills/local-review-codex/`) — same `REVIEW.md` policy, `gpt-5.6-sol`, `xhigh` reasoning; requires the `codex` CLI >= 0.144.1.
- **Domain guides**: `.claude/skills/native-trigger/` and `frontend/tutorial-system-guide.mdc`
- **CLI commands**: when adding/modifying/removing a command, subcommand, option, or description in `cli/src/commands/`, run `python system_prompts/generate.py` to refresh `system_prompts/auto-generated/` and `cli/src/guidance/skills.gen.ts`. The CLI docs the agents use to operate `wmill` are derived from the source — stale generated files give agents the wrong flags.
@@ -24,6 +24,8 @@ Open-source platform for internal tools, workflows, API integrations, background
## Dev Environment
- **Backend**: `cargo run` from `backend/` (API at http://localhost:8000)
- **DuckDB local jobs**: before running DuckDB scripts locally, build the FFI shared library with `cd backend/windmill-duckdb-ffi-internal && ./build_dev.sh`. Re-run it after clean builds or when `backend/target/debug/libwindmill_duckdb_ffi_internal.*` is missing. The bundled DuckDB compile (~2min) is cached in a per-user dir shared across worktrees, so a fresh worktree reuses it and the build is near-instant.
- **Data pipelines (DuckLake) from source**: a plain `cargo run` (even `--features quickjs`) advertises a `duckdb` worker tag but **cannot** execute DuckDB scripts and has **no** working S3 proxy (DuckLake writes 404). Build CE DuckLake with `cargo run --features quickjs,duckdb,parquet,private` (add `,python` for Python scripts, `,enterprise,license` for EE) **and** build the FFI (bullet above). See `backend/CLAUDE.md` → "Running data pipelines (DuckLake) from source" for the exact feature sets and the two feature-gate gotchas.
- **Frontend**: `REMOTE=http://localhost:8000 npm run dev` from `frontend/` (port 3000+)
- **Comments record constraints, not narration.** Write a comment only for what the code can't show: why a non-obvious approach is required, what breaks if it's "simplified" away. State each invariant once, at the place where someone would break it, in ≤4 lines. Don't describe what the next line does, don't repeat the same rationale at multiple sites, and don't address the PR reviewer (justifying a change belongs in the PR description, not the code). Describe the code as it is, never its drafting history: "we no longer do X", "unchanged behavior", "instead of the previous approach" are meaningless to a reader who never saw the earlier iteration — before finishing, reread your comments as if the current state is the only state that ever existed.
- **Ship only the tests the PR needs.** A committed test must pin behavior a future change could plausibly break, and be the smallest setup that exercises the new logic. While developing, write as many exhaustive tests and do as much manual testing as you need to convince yourself the change works — then remove that scaffolding before marking the PR ready, keeping only the essential regression guard(s). A test that merely re-exercises pre-existing behavior, or needs elaborate fixtures to assert something trivial, is scaffolding: delete it. If nothing meaningful is left to guard, ship no test rather than a ceremonial one.
- **Comments record constraints, not narration.** Write a comment only for what the code can't show: why a non-obvious approach is required, what breaks if it's "simplified" away. State each invariant once, at the place where someone would break it, in ≤4 lines. Don't describe what the next line does, don't repeat the same rationale at multiple sites, and don't address the PR reviewer (justifying a change belongs in the PR description, not the code). Reference nothing ephemeral — no numbered steps from your dev flow, no "the poller / the test does X" scaffolding, no transient state that won't exist for the next reader; keep only the essential, durable rationale. Describe the code as it is, never its drafting history: "we no longer do X", "unchanged behavior", "instead of the previous approach" are meaningless to a reader who never saw the earlier iteration — before finishing, reread your comments as if the current state is the only state that ever existed.
- **Never attribute work to a specific customer, account, or "requested by a customer" in repo-tracked content** (PR descriptions, commit messages, code comments, docs). Describe changes by their technical motivation instead.
* **nsjail:** make python/ansible rlimit_as configurable per worker (GIT-921) ([#10138](https://github.com/windmill-labs/windmill/issues/10138)) ([1787201](https://github.com/windmill-labs/windmill/commit/17872018cc037e699b1f6e1589d0ad05e0883cca))
### Bug Fixes
* **ai:** disable redirects on worker AI provider client (GHSA-5q4v) ([#10122](https://github.com/windmill-labs/windmill/issues/10122)) ([27ead8d](https://github.com/windmill-labs/windmill/commit/27ead8d0848cceacaf0c49fed0e8896472b851e3))
* **ai:** stop sending the AI agent system prompt twice for OpenAI ([#10126](https://github.com/windmill-labs/windmill/issues/10126)) ([8bfe5c9](https://github.com/windmill-labs/windmill/commit/8bfe5c93404ba3f137394f16d0571a06d891dc3b))
* **apps:** invalidate cached app policy on change or deletion (GHSA-r5v4-cxh9-7qhq) ([#10121](https://github.com/windmill-labs/windmill/issues/10121)) ([f7eb5c4](https://github.com/windmill-labs/windmill/commit/f7eb5c460d78792c24297e20cb062638522a4f68))
* **bash:** normalize CRLF line endings before running scripts ([#10131](https://github.com/windmill-labs/windmill/issues/10131)) ([6407d9f](https://github.com/windmill-labs/windmill/commit/6407d9ff5ce51e71ff8b8fc503d89a2bdc2e1761))
* **frontend:** graceful small-screen timeframe picker on the runs page ([#10073](https://github.com/windmill-labs/windmill/issues/10073)) ([af177ce](https://github.com/windmill-labs/windmill/commit/af177cefe07e6037e33cc90757088a98fb63a49f))
* **frontend:** keep session-exit URL clean by syncing new_draft strip with the router ([#10101](https://github.com/windmill-labs/windmill/issues/10101)) ([9705d60](https://github.com/windmill-labs/windmill/commit/9705d602848966f850613233d6e23b73a753c259))
* **frontend:** only carry custom-tag overrides on 'Run again' ([#10137](https://github.com/windmill-labs/windmill/issues/10137)) ([bd3adc9](https://github.com/windmill-labs/windmill/commit/bd3adc9781d8e77928c9feb03d0e05b63a1aaf7c))
* **frontend:** treat a displaced draft save as superseded, not failed ([#10094](https://github.com/windmill-labs/windmill/issues/10094)) ([2fe999f](https://github.com/windmill-labs/windmill/commit/2fe999f66cd15acd81850f970ada31e9892abff2))
* **nativets:** add Web Crypto support via deno_crypto ([#10109](https://github.com/windmill-labs/windmill/issues/10109)) ([ba23254](https://github.com/windmill-labs/windmill/commit/ba232544e7608731560defc0ed103c3e746e46ea))
* **nativets:** expose the standard web-platform globals deno_web provides ([#10112](https://github.com/windmill-labs/windmill/issues/10112)) ([6d1e12d](https://github.com/windmill-labs/windmill/commit/6d1e12d5e95a6e3fb74c142e33a141b5ce0856a9))
### Bug Fixes
* **jseval:** raise QuickJS eval memory cap to 128MB with clear OOM error ([#10116](https://github.com/windmill-labs/windmill/issues/10116)) ([95d9ff0](https://github.com/windmill-labs/windmill/commit/95d9ff02ee8a92162c06857bac7102c92c708c51))
* **ai-agent:** give tools a real description instead of the tool name ([#10083](https://github.com/windmill-labs/windmill/issues/10083)) ([7ebfad3](https://github.com/windmill-labs/windmill/commit/7ebfad382a2649a325444fcb745aca415871fe77))
* **ai-chat:** port flow-group and sticky-note instructions to global chat ([#10090](https://github.com/windmill-labs/windmill/issues/10090)) ([32f32d9](https://github.com/windmill-labs/windmill/commit/32f32d9a29bb214d6aae58c501b8892ceb1c6453))
* **cli:** add --tag override to script and flow run/preview ([#10079](https://github.com/windmill-labs/windmill/issues/10079)) ([98e6cca](https://github.com/windmill-labs/windmill/commit/98e6cca75d4dbc7417c7dd64289e0e8e56b84b0e))
* **sessions:** support many pending sessions persisted in IndexedDB ([#10076](https://github.com/windmill-labs/windmill/issues/10076)) ([bfcec7e](https://github.com/windmill-labs/windmill/commit/bfcec7e8ac71e86ab16d7db789556b5fc7cfd3c7))
### Bug Fixes
* **ai-chat:** size AI-created flow notes to fit their text ([#10091](https://github.com/windmill-labs/windmill/issues/10091)) ([af3e3fe](https://github.com/windmill-labs/windmill/commit/af3e3fe6674de21a03b5c157a649452320c1ed0e))
* **apps:** allow setting sandbox isolation and public access before first deploy ([#10085](https://github.com/windmill-labs/windmill/issues/10085)) ([cfc3f29](https://github.com/windmill-labs/windmill/commit/cfc3f292ad2fdc6067c558e42ef0754eca9469a9))
* **apps:** load themes when selecting the Resources → Theme tab ([#10086](https://github.com/windmill-labs/windmill/issues/10086)) ([f2869d8](https://github.com/windmill-labs/windmill/commit/f2869d8c1a6f84837168d59724a496b39080bcce))
* **frontend:** stop spurious raw-app reload that 404s on "Start without AI" ([#10099](https://github.com/windmill-labs/windmill/issues/10099)) ([2c702ef](https://github.com/windmill-labs/windmill/commit/2c702efec026bebdd9c1b8cdca4808e394b2fd09))
* **mcp:** align script auto_kind filter with scripts list API ([#10098](https://github.com/windmill-labs/windmill/issues/10098)) ([4edffeb](https://github.com/windmill-labs/windmill/commit/4edffeb84b5d691884dc3c274dfd8aa4e9441295))
* **sessions:** reopen script test panel when preview goes full screen ([#10082](https://github.com/windmill-labs/windmill/issues/10082)) ([eff9076](https://github.com/windmill-labs/windmill/commit/eff9076e9127eaa9b6a47897d5664932c720990b))
### Performance Improvements
* **runs:** index-bound batch re-run selection with a lossless completed_at bound (WIN-2168) ([#10074](https://github.com/windmill-labs/windmill/issues/10074)) ([15391f6](https://github.com/windmill-labs/windmill/commit/15391f6399eef5b85dac116b3ae04e71f70263a1))
* **ai-agent:** don't mark repeated tool calls as failed in flow graph ([#10075](https://github.com/windmill-labs/windmill/issues/10075)) ([207ce86](https://github.com/windmill-labs/windmill/commit/207ce8649cf7026c62eea2b1b2f462c7df8c4e5a))
* **triggers:** serve binary HTTP-route responses via base64 transfer encoding ([#10058](https://github.com/windmill-labs/windmill/issues/10058)) ([29f4cd4](https://github.com/windmill-labs/windmill/commit/29f4cd4b6f58a29b83b84a7a9b8a439d20ade00e)), closes [#5986](https://github.com/windmill-labs/windmill/issues/5986)
### Bug Fixes
* replicate all secrets on fork when external backend is configured ([#10060](https://github.com/windmill-labs/windmill/issues/10060)) ([92b7f37](https://github.com/windmill-labs/windmill/commit/92b7f375a90de2f78565ca06a13c79ff04eda44d))
* **apps:** authorize deployed-app S3 reads on-behalf of the author for logged-in viewers ([#10048](https://github.com/windmill-labs/windmill/issues/10048)) ([1e192f2](https://github.com/windmill-labs/windmill/commit/1e192f2d864b8a4671e900726972737406bc388a))
* **mcp:** add multi-workspace MCP tokens via the gateway endpoint ([#10043](https://github.com/windmill-labs/windmill/issues/10043)) ([8343203](https://github.com/windmill-labs/windmill/commit/8343203ec2cea28a2ffd5b4ac636497e861fa3ce))
### Bug Fixes
* clearer errors on auto-draft save failure (WIN-2157) ([#10053](https://github.com/windmill-labs/windmill/issues/10053)) ([04eb7dd](https://github.com/windmill-labs/windmill/commit/04eb7ddd3906c28bec1711e276473a87c7b9500f))
* **docker:** pin ansible tool interpreter to a persistent path ([#10054](https://github.com/windmill-labs/windmill/issues/10054)) ([6f49a1f](https://github.com/windmill-labs/windmill/commit/6f49a1f6a904442fcae9bb703f095b0a3ef61268))
* enforce read authorization when signing S3 objects ([#10049](https://github.com/windmill-labs/windmill/issues/10049)) ([5844c32](https://github.com/windmill-labs/windmill/commit/5844c32ac5d08081b3de7f3d11b8b98eb1e1ad9a))
* **frontend:** don't re-seed empty editor on stale ?new_draft after draft exists ([#10044](https://github.com/windmill-labs/windmill/issues/10044)) ([e668193](https://github.com/windmill-labs/windmill/commit/e668193a93b4a7df50459b31f2dc5f9a9b23d0fe))
* **frontend:** mint draft path for new SDK builder items so autosave attaches ([#10056](https://github.com/windmill-labs/windmill/issues/10056)) ([a89b896](https://github.com/windmill-labs/windmill/commit/a89b896ce5638f42f334055f9ffe6971b047aa84))
* **frontend:** show nested restart button for subflows nested in containers ([#10042](https://github.com/windmill-labs/windmill/issues/10042)) ([3b07817](https://github.com/windmill-labs/windmill/commit/3b0781761b70667c5961bcb15d41c907716fa9e7))
* **frontend:** show optimistic user message and fork-creation label before beforeSend ([#10037](https://github.com/windmill-labs/windmill/issues/10037)) ([1c88242](https://github.com/windmill-labs/windmill/commit/1c88242849a02b927f59e0a67c4b4707371b784f))
* add multi-select mode to copilot askUserQuestion ([#10016](https://github.com/windmill-labs/windmill/issues/10016)) ([7569798](https://github.com/windmill-labs/windmill/commit/756979852c3245d06c5c73eb60e9a09fd59635c5))
### Bug Fixes
* accept bunnative language in AI chat flow step validation ([#10030](https://github.com/windmill-labs/windmill/issues/10030)) ([5a460db](https://github.com/windmill-labs/windmill/commit/5a460dbec6e2b81e01aa2c36cb504dde4ff6b24a))
* **frontend:** name the draft in AI chat test-run confirmation ([#10024](https://github.com/windmill-labs/windmill/issues/10024)) ([9036ac7](https://github.com/windmill-labs/windmill/commit/9036ac789f358103b8d639a6b94708c452f52f90))
* **frontend:** open new script/flow/app in AI session (not-found + friendly tab) ([#10028](https://github.com/windmill-labs/windmill/issues/10028)) ([0353569](https://github.com/windmill-labs/windmill/commit/03535691d607d8a9d1c1fa1c9c284fa3b6051d40))
* **frontend:** scope raw-app, flow and script editors to the session workspace ([#10015](https://github.com/windmill-labs/windmill/issues/10015)) ([c000bbc](https://github.com/windmill-labs/windmill/commit/c000bbca283f5d61cff8a39458764b2b2dd2b58f))
* resolve fork family/picker for superadmin visiting a non-member workspace ([#10023](https://github.com/windmill-labs/windmill/issues/10023)) ([368fd2d](https://github.com/windmill-labs/windmill/commit/368fd2d9e4b3ffb66e64934d9a622eea291cde5a))
* scope AI-session flow/script editors to the session workspace ([#10025](https://github.com/windmill-labs/windmill/issues/10025)) ([c5060a1](https://github.com/windmill-labs/windmill/commit/c5060a1e9af5a704e90f92d325abf29626ecd28a))
* **security:** drop --allow-run from Deno sandbox (GHSA-gj6h-vw66-mr8f) ([#10039](https://github.com/windmill-labs/windmill/issues/10039)) ([c029d6d](https://github.com/windmill-labs/windmill/commit/c029d6dcde44a3d16dee23a80afad920a0535b73))
* **sessions:** open test pane when enabling debug so the debug UI is visible ([#9998](https://github.com/windmill-labs/windmill/issues/9998)) ([d7a9b46](https://github.com/windmill-labs/windmill/commit/d7a9b46ab95108c7669b47b7c4be6d8c7964a9e6))
* sync theme into session page preview iframes on toggle ([#10018](https://github.com/windmill-labs/windmill/issues/10018)) ([3704d00](https://github.com/windmill-labs/windmill/commit/3704d00956dea3b8e562a894d5330e052a753cb3))
### Performance Improvements
* index v2_job(parent_job) to speed up run child-job listing ([#10034](https://github.com/windmill-labs/windmill/issues/10034)) ([9feda57](https://github.com/windmill-labs/windmill/commit/9feda57c15bddc7ef481579b73636b88c2a143c5))
* AI chat background jobs tray with detach, approval and preview ([#9982](https://github.com/windmill-labs/windmill/issues/9982)) ([286da00](https://github.com/windmill-labs/windmill/commit/286da005ef2faad1d193640f74f8e96999358707))
* condensed top bar for session preview editors ([#10011](https://github.com/windmill-labs/windmill/issues/10011)) ([b847ca2](https://github.com/windmill-labs/windmill/commit/b847ca2bc7f06f494aa802d4350d6f032ba2bb58))
* bump bundled Go CLIs to patched versions to clear image CVEs ([#9996](https://github.com/windmill-labs/windmill/issues/9996)) ([d467161](https://github.com/windmill-labs/windmill/commit/d467161117444d7d9b18def627e90d9622512e02))
* name the offending item when a fork fails on a NUL escape ([#10013](https://github.com/windmill-labs/windmill/issues/10013)) ([99d0047](https://github.com/windmill-labs/windmill/commit/99d00475156def6faad255c4e728923253f9169f))
* preserve worker group tag override on 'Run again' ([#10004](https://github.com/windmill-labs/windmill/issues/10004)) ([c4cb2f3](https://github.com/windmill-labs/windmill/commit/c4cb2f373b6361f0f3ce6b1c8e32a4c010207760))
* replicate external secret backend secrets when forking a workspace ([#10007](https://github.com/windmill-labs/windmill/issues/10007)) ([f65fe7b](https://github.com/windmill-labs/windmill/commit/f65fe7bf585d353f7d88746e947d68e2f351e516))
* session preview editors and picker dropdown overflow ([#10010](https://github.com/windmill-labs/windmill/issues/10010)) ([fb12b23](https://github.com/windmill-labs/windmill/commit/fb12b23e0169ba2cdcf454a27dcf814a2caf26b3))
* add fork_parent_workspace claim to OIDC tokens for fork workspaces ([#9987](https://github.com/windmill-labs/windmill/issues/9987)) ([7efeae2](https://github.com/windmill-labs/windmill/commit/7efeae26d821b10667b6e3edd220468f6ae48936))
* add SQL migrations for data tables ([#9693](https://github.com/windmill-labs/windmill/issues/9693)) ([e47aeda](https://github.com/windmill-labs/windmill/commit/e47aedac0a4af40dd697d5fc4d54dd3c8efe9ab8))
* **cli:** clarify fork-branch workspace auto-targeting in output ([#9988](https://github.com/windmill-labs/windmill/issues/9988)) ([88c2d0e](https://github.com/windmill-labs/windmill/commit/88c2d0e8e32c218787c01daed80ef41efc39dd11))
* open runs/schedules pages from AI chat in session preview tabs ([#9976](https://github.com/windmill-labs/windmill/issues/9976)) ([4bb82ad](https://github.com/windmill-labs/windmill/commit/4bb82ad6cdb62eae7b69b1054714333e54558632))
* **raw-apps:** runtime-error overlay + AI import-React instruction ([#9966](https://github.com/windmill-labs/windmill/issues/9966)) ([8df613b](https://github.com/windmill-labs/windmill/commit/8df613b4d2f88765f49cc988a894ca323c4ec4f7))
* **sessions:** v2 unified sidebar with family/fork scoping and preview router ([#9816](https://github.com/windmill-labs/windmill/issues/9816)) ([9503190](https://github.com/windmill-labs/windmill/commit/95031903ebe223dc03b49a6bcd3e4ee67cefc4bb))
* smooth bursty AI chat streaming with a typewriter reveal ([#9991](https://github.com/windmill-labs/windmill/issues/9991)) ([a6276b5](https://github.com/windmill-labs/windmill/commit/a6276b590082d06480434a8ea002c335ea1cfb59))
* update base image to debian 13 (trixie) ([#9973](https://github.com/windmill-labs/windmill/issues/9973)) ([c5c1ead](https://github.com/windmill-labs/windmill/commit/c5c1eadeb18e509a98d1e787206c0438417683fc))
### Bug Fixes
* **ai-agent:** align agent_actions_success with agent_actions for mcp and websearch ([#9983](https://github.com/windmill-labs/windmill/issues/9983)) ([87f8d46](https://github.com/windmill-labs/windmill/commit/87f8d46aafffd5e88a336192c51e0c95ff2e6f18))
* **ai:** flow writer builds approval steps as scripts, not identity ([#9985](https://github.com/windmill-labs/windmill/issues/9985)) ([6b01caa](https://github.com/windmill-labs/windmill/commit/6b01caaf26a4f0a08f643db4e22a70e27d0dc554))
* clear old path asset usage when renaming a script ([#9979](https://github.com/windmill-labs/windmill/issues/9979)) ([927b8d0](https://github.com/windmill-labs/windmill/commit/927b8d064f693384978184992b8f8a1cd708e711))
* **pipelines:** live materialize/dataset editing — stale graph, phantom drafts, stale Save-all deploys ([#9990](https://github.com/windmill-labs/windmill/issues/9990)) ([f7efb64](https://github.com/windmill-labs/windmill/commit/f7efb646bf1f2e132d1e3ff031b142383ae01c5e))
* add cosmetic dev/staging label for dev workspaces ([#9959](https://github.com/windmill-labs/windmill/issues/9959)) ([fd8e64d](https://github.com/windmill-labs/windmill/commit/fd8e64d11fea3ffdb7858100c67cb2e9ca841ed6))
* **ai:** centralize Anthropic Messages API routing across completion paths ([#9960](https://github.com/windmill-labs/windmill/issues/9960)) ([cc2f638](https://github.com/windmill-labs/windmill/commit/cc2f638de6cebeffb9fee1d4835a0cfd565af86c))
* **assets:** responsive layout for small screens ([#9961](https://github.com/windmill-labs/windmill/issues/9961)) ([45946d1](https://github.com/windmill-labs/windmill/commit/45946d1185c0bd07948d4d8454880c2801571f9d))
* critical alerts modal mute toggles no longer close popover or fail to save ([#9969](https://github.com/windmill-labs/windmill/issues/9969)) ([6587019](https://github.com/windmill-labs/windmill/commit/6587019d263374ee5707d258f5d8eec7e73c690d))
* **pipelines:** require data uploads before running a pipeline ([#9953](https://github.com/windmill-labs/windmill/issues/9953)) ([a1c5b7a](https://github.com/windmill-labs/windmill/commit/a1c5b7aa3ed2841f09f4f148ded5c5b5ef10fd3d))
* **pipelines:** wm_partition macro for grain-agnostic partition filters ([#9950](https://github.com/windmill-labs/windmill/issues/9950)) ([43044c2](https://github.com/windmill-labs/windmill/commit/43044c2e28139b1dbde6844c781a821f8de68f58))
### Bug Fixes
* **ai:** test key routes Azure Foundry Claude models via Anthropic Messages API ([#9956](https://github.com/windmill-labs/windmill/issues/9956)) ([ea19cc9](https://github.com/windmill-labs/windmill/commit/ea19cc9dc459bd259e27f7fcc29601a010c5f8f0))
* **pipelines:** make node & pipeline-level run affordances always visible ([#9948](https://github.com/windmill-labs/windmill/issues/9948)) ([6eabb96](https://github.com/windmill-labs/windmill/commit/6eabb96ae78fb966f9916f907bb693d569b04c0b))
* read chat drafts via own-draft route so drawer-kind drafts deploy ([#9913](https://github.com/windmill-labs/windmill/issues/9913)) ([056ebdb](https://github.com/windmill-labs/windmill/commit/056ebdb03543a93094c80ca354c117236cd8d6c8))
* resolve extensionless bun relative imports on windows loader ([#9949](https://github.com/windmill-labs/windmill/issues/9949)) ([bf96621](https://github.com/windmill-labs/windmill/commit/bf9662172ad7e0ff53d39adc338fd7886672c8f9))
* **cli:** macro-library parity in --local pipeline graph + read-only run --dry-run ([#9942](https://github.com/windmill-labs/windmill/issues/9942)) ([e3f4303](https://github.com/windmill-labs/windmill/commit/e3f43033cafcdb5df253aeb55ce93e599b2584d2))
* **datatable:** self-teaching error for unresolved datatable:// references ([#9941](https://github.com/windmill-labs/windmill/issues/9941)) ([55451db](https://github.com/windmill-labs/windmill/commit/55451db009e3060c21948ece2c97e102a3c9b171))
* **object-storage:** remove 20-file bucket-browser listing cap in CE ([#9935](https://github.com/windmill-labs/windmill/issues/9935)) ([22452ce](https://github.com/windmill-labs/windmill/commit/22452ce54034a9bea8f7d48946818fd148b938c0))
* **pipelines:** link SCD2 <dim>_current view to its producer across all graph surfaces ([#9933](https://github.com/windmill-labs/windmill/issues/9933)) ([574d3ac](https://github.com/windmill-labs/windmill/commit/574d3ac9ff5015b5d3f53040c9d4dfbfd161a076))
* **pipelines:** order data_test relationships refs before the tested script in a cascade ([#9934](https://github.com/windmill-labs/windmill/issues/9934)) ([46be39d](https://github.com/windmill-labs/windmill/commit/46be39dfb7fbfb2b70e61819d6065b45810c41c9))
* rebuild windows bun loader main.ts filter from forward-slash cdir ([#9946](https://github.com/windmill-labs/windmill/issues/9946)) ([a582e04](https://github.com/windmill-labs/windmill/commit/a582e04bf40cf685f88bceaf88e3d24bde3d420a))
* **ai-agent:** support reasoning effort in AI agent workflow steps ([#9886](https://github.com/windmill-labs/windmill/issues/9886)) ([a368d49](https://github.com/windmill-labs/windmill/commit/a368d49bd8786a2dca6771f2051f1d44d1b2363d))
* **pipelines:** capture violating-row samples for data tests ([#9919](https://github.com/windmill-labs/windmill/issues/9919)) ([d4b4374](https://github.com/windmill-labs/windmill/commit/d4b4374de8f8a7875b050c16d1236fcd0355812b))
* **pipelines:** fork data environments for ducklake materialization (dev data) ([#9915](https://github.com/windmill-labs/windmill/issues/9915)) ([39eb9de](https://github.com/windmill-labs/windmill/commit/39eb9de1bce400109c130a081807e40e995ae068))
* **pipelines:** record upstream snapshot ids on cascade-dispatched jobs ([#9910](https://github.com/windmill-labs/windmill/issues/9910)) ([af36498](https://github.com/windmill-labs/windmill/commit/af36498432e643108308e1c03b5d986d0f0f8888))
* **pipeline:** write-audit-publish for materialization data tests ([#9911](https://github.com/windmill-labs/windmill/issues/9911)) ([dce247c](https://github.com/windmill-labs/windmill/commit/dce247c6d2678a2c95bd728027e17ae3965638e2))
* **cli:** publish all windmill-parser-wasm-* deps so local pipeline graph keeps write edges ([#9926](https://github.com/windmill-labs/windmill/issues/9926)) ([744a759](https://github.com/windmill-labs/windmill/commit/744a7597edaf3ca9a7fd2b21a34fb33457913a64))
* **frontend:** add federatedTokenFile field to instance object storage Azure config ([#9904](https://github.com/windmill-labs/windmill/issues/9904)) ([ae85d27](https://github.com/windmill-labs/windmill/commit/ae85d274371a24c5badb6081f00deeb409123252))
### Bug Fixes
* **ai:** route Azure Foundry Claude models via Anthropic Messages API ([#9908](https://github.com/windmill-labs/windmill/issues/9908)) ([d600c7e](https://github.com/windmill-labs/windmill/commit/d600c7ecfe305533798e82e8d05e5f2f297f9b54))
* **forks:** clone only the current raw-app bundle, via server-side copy ([#9899](https://github.com/windmill-labs/windmill/issues/9899)) ([5c521d8](https://github.com/windmill-labs/windmill/commit/5c521d808a2b5d6d6bb7cf3da17fb2addc53fdf4))
* **kafka:** set https.ca.location=probe for OAUTHBEARER OIDC token endpoint ([#9897](https://github.com/windmill-labs/windmill/issues/9897)) ([1b6065f](https://github.com/windmill-labs/windmill/commit/1b6065fa9201fd548c4b2ef199f1009200645929))
* prevent truncated tool call args from bricking AI chat sessions ([#9902](https://github.com/windmill-labs/windmill/issues/9902)) ([4ba17d0](https://github.com/windmill-labs/windmill/commit/4ba17d0f9cd70489f89c84f982a0c8f0062fed1a))
* strip NUL characters from app values at save time ([#9903](https://github.com/windmill-labs/windmill/issues/9903)) ([3ec1f16](https://github.com/windmill-labs/windmill/commit/3ec1f164be9c8c6c40e003188ce593a963c65a43))
* **ai:** add Azure AI Foundry as a native AI provider ([#9879](https://github.com/windmill-labs/windmill/issues/9879)) ([d9b080f](https://github.com/windmill-labs/windmill/commit/d9b080f57fa0be144cefa773d39742c45b40f043))
* **frontend:** group compare & deploy items by folder ([#9880](https://github.com/windmill-labs/windmill/issues/9880)) ([7b04820](https://github.com/windmill-labs/windmill/commit/7b04820f8ef8c7f02f79dd4239a877f667d23e6a))
* **frontend:** pipelines index page and sql editor hint ([#9881](https://github.com/windmill-labs/windmill/issues/9881)) ([20351a6](https://github.com/windmill-labs/windmill/commit/20351a6b4c262184c5f815eeb5de007ab1eaf4a0))
* **pipeline:** backfill a range of partitions from the asset drawer ([#9885](https://github.com/windmill-labs/windmill/issues/9885)) ([53bbb92](https://github.com/windmill-labs/windmill/commit/53bbb92953178eb6d0017818ef870f3cb2399dfd))
* **s3:** replace CE 50MB upload cap with 10GiB workspace storage quota ([#9874](https://github.com/windmill-labs/windmill/issues/9874)) ([af01e90](https://github.com/windmill-labs/windmill/commit/af01e90b5c65d1b1cfacf4433f8cff7effe73768))
* support workspace forks on cloud using parent workspace limits ([#9864](https://github.com/windmill-labs/windmill/issues/9864)) ([7c7d747](https://github.com/windmill-labs/windmill/commit/7c7d7474cc86a4052272032f281cc4d7a85db37b))
* **frontend:** home New submenus fall back below, hugging the right edge ([#9894](https://github.com/windmill-labs/windmill/issues/9894)) ([186ac49](https://github.com/windmill-labs/windmill/commit/186ac4933b79aed57fce23ebcf3b525fcfd1c474))
* **frontend:** show inline workspace name editor on general settings (Fixes GIT-911) ([#9892](https://github.com/windmill-labs/windmill/issues/9892)) ([a49c087](https://github.com/windmill-labs/windmill/commit/a49c0871d7ab2aaf78a7713b8a786ead937434da))
* **frontend:** stack cron field and cron builder button on narrow screens ([#9871](https://github.com/windmill-labs/windmill/issues/9871)) ([7989795](https://github.com/windmill-labs/windmill/commit/79897950e7646b00d92a28a009174d91c705b251))
* invalidate bun bundle cache on transitive relative-import changes ([#9891](https://github.com/windmill-labs/windmill/issues/9891)) ([d15033c](https://github.com/windmill-labs/windmill/commit/d15033cde6a474b548ebbaf18ff02223fc21f701))
* make SMTP username and password optional in frontend validation ([#9895](https://github.com/windmill-labs/windmill/issues/9895)) ([37bb574](https://github.com/windmill-labs/windmill/commit/37bb57474e8336823bb31527f2a708ef41cd39c4))
* **parsers:** infer py s3 assets from S3Object constructor and dict forms ([#9877](https://github.com/windmill-labs/windmill/issues/9877)) ([659642e](https://github.com/windmill-labs/windmill/commit/659642e4889361f86e8addb038cda62fc3471006))
* stale AI chat context picker after workspace item changes ([#9893](https://github.com/windmill-labs/windmill/issues/9893)) ([5af91a6](https://github.com/windmill-labs/windmill/commit/5af91a677cad88faccba702e3556fc4fb7b6e640))
* **frontend:** add zoom and download to Mermaid graphs ([#9859](https://github.com/windmill-labs/windmill/issues/9859)) ([289017b](https://github.com/windmill-labs/windmill/commit/289017bcb28c049c8258b2ffd7da0ec3e6ef120b))
* use derived username instead of email for non-member superadmins ([#9857](https://github.com/windmill-labs/windmill/issues/9857)) ([76a9523](https://github.com/windmill-labs/windmill/commit/76a95230095ca3f43c9dc9eecde0e9de6520242f))
* **folders:** allow dots and at-signs in folder owner validation ([#9856](https://github.com/windmill-labs/windmill/issues/9856)) ([383c705](https://github.com/windmill-labs/windmill/commit/383c70523bf81c5c07784a4379ef6b1c93ff86e5))
* **forks:** require admin of both sides for the compare visibility guard ([#9869](https://github.com/windmill-labs/windmill/issues/9869)) ([7363d2c](https://github.com/windmill-labs/windmill/commit/7363d2c217cb04391f03b2f9958be70a9d0b5325))
* **forks:** reset diff tally on trigger delete + guard compare visibility for admins ([#9866](https://github.com/windmill-labs/windmill/issues/9866)) ([6a6f129](https://github.com/windmill-labs/windmill/commit/6a6f12960e29c314d11ad541519c71412f42567b))
* **jobs:** give flow dynselect a path and its worker tag, like scripts ([#9867](https://github.com/windmill-labs/windmill/issues/9867)) ([1a9debb](https://github.com/windmill-labs/windmill/commit/1a9debb689f756f38db085d1360c8fe7691ada48))
* **offboarding:** make global reassignment per-workspace and optional ([#9863](https://github.com/windmill-labs/windmill/issues/9863)) ([3586164](https://github.com/windmill-labs/windmill/commit/35861641f807a02b5c205608fb592e20ee7cad7f))
* add copy-to-clipboard button to rendered Mermaid diagrams in AI chat ([#9838](https://github.com/windmill-labs/windmill/issues/9838)) ([a27e814](https://github.com/windmill-labs/windmill/commit/a27e814a03c615259381eaf684aa90d56569b0af))
* add dev workspaces paired with a lockable prod workspace ([#9793](https://github.com/windmill-labs/windmill/issues/9793)) ([b4b0c6a](https://github.com/windmill-labs/windmill/commit/b4b0c6a93e52152251fadefe319773faf42549b2))
* **ansible:** support repo-provided ansible.cfg in delegate_to_git_repo ([#9851](https://github.com/windmill-labs/windmill/issues/9851)) ([68bf0da](https://github.com/windmill-labs/windmill/commit/68bf0daf5815307cda6ce23214dd5159b6aa33b4))
* **licensing:** enforce offline license seat cap ([#9845](https://github.com/windmill-labs/windmill/issues/9845)) ([83f3d7f](https://github.com/windmill-labs/windmill/commit/83f3d7f910b331c09f60cc9ff556728afa3dec07))
* **object-store:** make GCS service account key optional for Workload Identity ([#9842](https://github.com/windmill-labs/windmill/issues/9842)) ([83ed011](https://github.com/windmill-labs/windmill/commit/83ed011e264f20ffa66a7bf933f2fe3615cf6b67))
* **pipeline:** local development for data pipelines (CLI --local + pipeline dev preview) ([#9840](https://github.com/windmill-labs/windmill/issues/9840)) ([74f579e](https://github.com/windmill-labs/windmill/commit/74f579e6d9ef08e74460f904a4c22ed9d6a3b5b0))
* **pipelines:** add managed SCD2 history materialize strategy ([#9850](https://github.com/windmill-labs/windmill/issues/9850)) ([5a66127](https://github.com/windmill-labs/windmill/commit/5a661279a3690e2393b9b16996f5d1a5a509259c))
* grant dispatch_event table to windmill roles ([#9852](https://github.com/windmill-labs/windmill/issues/9852)) ([f05b50d](https://github.com/windmill-labs/windmill/commit/f05b50d29ac2fdbb808a97057fb92c8e425b4a2f))
* grant workspace_diff, materialized_partition, debounce_stale_data to windmill roles ([#9853](https://github.com/windmill-labs/windmill/issues/9853)) ([293647d](https://github.com/windmill-labs/windmill/commit/293647de4c13cb8468cbd81ff1924cba90e164b4))
* honor verify-ca/verify-full sslmode for postgres connections ([#9835](https://github.com/windmill-labs/windmill/issues/9835)) ([bf6be96](https://github.com/windmill-labs/windmill/commit/bf6be967fa8c74e1299cf63f813c1cfa34b97f3e))
* **s3_proxy:** preserve URL-encoding on Hive-partition proxy writes ([#9848](https://github.com/windmill-labs/windmill/issues/9848)) ([6b79bdd](https://github.com/windmill-labs/windmill/commit/6b79bddd42fe55f891c17cb71a7e36ee31337bac))
* validate workspace name length (max 50 chars) on create and fork ([#9854](https://github.com/windmill-labs/windmill/issues/9854)) ([b52972d](https://github.com/windmill-labs/windmill/commit/b52972d0de89004e98d18241d238ca028e4eecba))
* **home:** redesign create-new popover and home header ([#9827](https://github.com/windmill-labs/windmill/issues/9827)) ([2493eaf](https://github.com/windmill-labs/windmill/commit/2493eaf031f30072637a297398674e761f039005))
* **audit:** don't read pg_authid from an elevated context in S3 export migration ([#9832](https://github.com/windmill-labs/windmill/issues/9832)) ([75ba81b](https://github.com/windmill-labs/windmill/commit/75ba81b2d27fb0722095780312064cb93d20287e))
* close unauthenticated DAP debugger program-mode launch bypass ([#9829](https://github.com/windmill-labs/windmill/issues/9829)) ([c0768de](https://github.com/windmill-labs/windmill/commit/c0768de0acdf63eaba5fb97d04bfc64f2f03b93d))
* redeploy older app version from deployment history ([#9826](https://github.com/windmill-labs/windmill/issues/9826)) ([c479afa](https://github.com/windmill-labs/windmill/commit/c479afab8ebceccbee050e923dc5c27a6712ea62))
* **ai-chat:** add create_folder tool to global chat ([#9819](https://github.com/windmill-labs/windmill/issues/9819)) ([44c25de](https://github.com/windmill-labs/windmill/commit/44c25de418612ab98341adb15d5671222b54367e))
* **ai-chat:** hint /compact in context usage tooltip ([#9777](https://github.com/windmill-labs/windmill/issues/9777)) ([aadfb62](https://github.com/windmill-labs/windmill/commit/aadfb620c0b7dcd7e94367b761875e14ef9abe69))
* **ai-chat:** let global chat edit the user's personal instructions ([#9771](https://github.com/windmill-labs/windmill/issues/9771)) ([3be2752](https://github.com/windmill-labs/windmill/commit/3be27521b05de33e48582e80c6651071f889f048))
* **ai-chat:** surface raw apps in the @-mention context picker ([#9800](https://github.com/windmill-labs/windmill/issues/9800)) ([1602244](https://github.com/windmill-labs/windmill/commit/16022447c7b445be753b9545b10b4c67da0893d5))
* **sdk:** allow overriding worker tag when running jobs (WIN-2105) ([#9807](https://github.com/windmill-labs/windmill/issues/9807)) ([52fc7bf](https://github.com/windmill-labs/windmill/commit/52fc7bf94cf3f87f68d9dba9884944d87e7d5d57))
### Bug Fixes
* apply step timeout to 'Test this step' preview ([#9810](https://github.com/windmill-labs/windmill/issues/9810)) ([d04062b](https://github.com/windmill-labs/windmill/commit/d04062bff58c9c4c79ce542a4321e71bcbcf0e98))
* **frontend:** clarify instance data table unavailable on cloud ([#9806](https://github.com/windmill-labs/windmill/issues/9806)) ([c3e8c78](https://github.com/windmill-labs/windmill/commit/c3e8c789ac05c9c28991d9ab6f2358f61fa87971))
* hide GCS service account key behind a reveal in object storage settings ([#9815](https://github.com/windmill-labs/windmill/issues/9815)) ([0ec5061](https://github.com/windmill-labs/windmill/commit/0ec5061270749ed078e01f5a4bc7397a1755ca32))
* ping job during volume setup to prevent false zombie restarts ([#9803](https://github.com/windmill-labs/windmill/issues/9803)) ([43bb676](https://github.com/windmill-labs/windmill/commit/43bb676dc5652cb06fe1414b8d3aacf295bae36b))
* skipped suspend step no longer parks the flow forever ([#9821](https://github.com/windmill-labs/windmill/issues/9821)) ([40110bc](https://github.com/windmill-labs/windmill/commit/40110bc7158bc42c3d84bd4637a12b82fcd72a9a))
* data tests for ducklake pipeline materialization ([#9708](https://github.com/windmill-labs/windmill/issues/9708)) ([f6998ec](https://github.com/windmill-labs/windmill/commit/f6998ec54cba2507703790bf33427e7567d42c4b))
* detect and guard against deploying stale drafts ([#9768](https://github.com/windmill-labs/windmill/issues/9768)) ([d865518](https://github.com/windmill-labs/windmill/commit/d8655189347f58df9d17e83dc55798baf7964279))
* ducklake time-travel UX (snapshot history + AT VERSION reads) ([#9709](https://github.com/windmill-labs/windmill/issues/9709)) ([d131d75](https://github.com/windmill-labs/windmill/commit/d131d754e1fc9674abf5de383d2bc93596df9bd1))
* self-host docs search for chat, mcp, cli; drop inkeep ([#9772](https://github.com/windmill-labs/windmill/issues/9772)) ([9d61e4e](https://github.com/windmill-labs/windmill/commit/9d61e4e59e4101de84217f7c7846f1aa94e84d89))
### Bug Fixes
* allow hyphens in postgresql database name validation ([#9782](https://github.com/windmill-labs/windmill/issues/9782)) ([170cd79](https://github.com/windmill-labs/windmill/commit/170cd79aaf92152fc3c0f675f155853c7f0e5b25))
* **debounce:** never supersede a running debounce survivor ([#9780](https://github.com/windmill-labs/windmill/issues/9780)) ([5549bdc](https://github.com/windmill-labs/windmill/commit/5549bdc67a5559a764616c44b1018543bc0568fe))
* decrypt secret variables via external backend in common resolvers ([#9784](https://github.com/windmill-labs/windmill/issues/9784)) ([cd42c6c](https://github.com/windmill-labs/windmill/commit/cd42c6ca18261328055554788932c3fe876a4a5b))
* enforce containment of python module dir for preview jobs ([#9704](https://github.com/windmill-labs/windmill/issues/9704)) ([88fca6a](https://github.com/windmill-labs/windmill/commit/88fca6a8c130b9e3b0f0cd410e422d4e074fc11f))
* **frontend:** nested-loop "Test this step" resolves iter to innermost loop ([#9778](https://github.com/windmill-labs/windmill/issues/9778)) ([74ebfc6](https://github.com/windmill-labs/windmill/commit/74ebfc67f069047875db738926865bd4bd6fe9e9))
* opt out of Deno minimum-dependency-age for private npm registries ([#9802](https://github.com/windmill-labs/windmill/issues/9802)) ([b28f974](https://github.com/windmill-labs/windmill/commit/b28f974e5069f635419d9ea56fad6a0e417894e8))
* pass SSL cert env vars to `uv python install` ([#9790](https://github.com/windmill-labs/windmill/issues/9790)) ([962758c](https://github.com/windmill-labs/windmill/commit/962758c02de5f6d962c681fe9c39769b99429e8d))
* **python:** re-verify wheel RECORD on local cache reuse (once per worker) ([#9775](https://github.com/windmill-labs/windmill/issues/9775)) ([6c71c33](https://github.com/windmill-labs/windmill/commit/6c71c33470e3ea547f3b994db829eb4d04882443))
* **python:** serialize concurrent installs into shared wheel cache dir ([#9787](https://github.com/windmill-labs/windmill/issues/9787)) ([11d83ab](https://github.com/windmill-labs/windmill/commit/11d83ab1ec559be5d3263010228e6db65358e04b))
* re-pin stale-draft fork base when restoring an app deployment ([#9792](https://github.com/windmill-labs/windmill/issues/9792)) ([b9711e5](https://github.com/windmill-labs/windmill/commit/b9711e5ace8585315a1c2b85bb25ac8dd7832d6f))
* restore libargon2-1 for PHP runtime in server image ([#9795](https://github.com/windmill-labs/windmill/issues/9795)) ([e9cb806](https://github.com/windmill-labs/windmill/commit/e9cb80639b2dec63ede69fc3a4e3720bb1a3c319))
* use transaction for parallel_monitor_lock DELETE in last-iteration path ([#9789](https://github.com/windmill-labs/windmill/issues/9789)) ([754cae9](https://github.com/windmill-labs/windmill/commit/754cae956ac8d431ddeb055453835e249cdd07b7))
### Performance Improvements
* drop v2_job side-table ON DELETE CASCADE FKs to speed retention deletes ([#9786](https://github.com/windmill-labs/windmill/issues/9786)) ([aa098c7](https://github.com/windmill-labs/windmill/commit/aa098c70c0271b2b1917749d1f607c0559cf04de))
* eliminate dual-connection DB pool contention across worker, queue, and api ([#9798](https://github.com/windmill-labs/windmill/issues/9798)) ([0dbd9c1](https://github.com/windmill-labs/windmill/commit/0dbd9c1231b00d4693af68835fe1d9e7c8869b43))
* **ai-chat:** add /clear session command to start a fresh conversation ([#9769](https://github.com/windmill-labs/windmill/issues/9769)) ([3fafac2](https://github.com/windmill-labs/windmill/commit/3fafac275d2100a6f89924040650cf959945d209))
* **ai-chat:** context usage gauge + unified model settings menu ([#9763](https://github.com/windmill-labs/windmill/issues/9763)) ([2e020b2](https://github.com/windmill-labs/windmill/commit/2e020b2ccc7a649a5923bff72a98f07d4fc85381))
* **apps:** show raw-app fork diffs as per-file tree items ([#9491](https://github.com/windmill-labs/windmill/issues/9491)) ([e98df38](https://github.com/windmill-labs/windmill/commit/e98df38ac43823ee85209a4b09cd70690469302d))
* **frontend:** add filter submenu to collapsed AI sessions popover ([#9757](https://github.com/windmill-labs/windmill/issues/9757)) ([3d48ba7](https://github.com/windmill-labs/windmill/commit/3d48ba7738c3d3356539b5fc44a871f6b7f9d548))
* **frontend:** restore raw app 'open preview in separate window' ([#9765](https://github.com/windmill-labs/windmill/issues/9765)) ([a116715](https://github.com/windmill-labs/windmill/commit/a116715c418c39d48a91e6c0b4484a31537dff38))
* **frontend:** show approval wait as a distinct segment in flow timeline ([#9756](https://github.com/windmill-labs/windmill/issues/9756)) ([2a70ccc](https://github.com/windmill-labs/windmill/commit/2a70ccc38675c7c2353807a4f85764a8a35224e2))
* scope AI sessions per workspace root with lifecycle reconcile ([#9734](https://github.com/windmill-labs/windmill/issues/9734)) ([42c5e7a](https://github.com/windmill-labs/windmill/commit/42c5e7a3fc9b74256de8806ec0d7b62e8bbf029c))
### Bug Fixes
* **ai-chat:** strip unclosed <summary> tag leaking into compaction summary ([#9750](https://github.com/windmill-labs/windmill/issues/9750)) ([250a05f](https://github.com/windmill-labs/windmill/commit/250a05f544ae397bb91af5fc83bf408cfe1c554d))
* forbid superadmin job tokens from global user and token management ([#9715](https://github.com/windmill-labs/windmill/issues/9715)) ([043c2c0](https://github.com/windmill-labs/windmill/commit/043c2c05b7678c49faca0ccb28e5f6393567ba4d))
* **frontend:** highlight the runtime-chosen branch in flow graph viewer ([#9755](https://github.com/windmill-labs/windmill/issues/9755)) ([de6192b](https://github.com/windmill-labs/windmill/commit/de6192bec1695883a07452f7db2fb51c94dbfd43))
* **frontend:** show AI skills settings only when global mode enabled ([#9747](https://github.com/windmill-labs/windmill/issues/9747)) ([c017f7f](https://github.com/windmill-labs/windmill/commit/c017f7f8919a51292ddf01574961d1774bc1ba23))
* **frontend:** stop flow step id generation from being poisoned by non-canonical keys ([#9766](https://github.com/windmill-labs/windmill/issues/9766)) ([4dbf873](https://github.com/windmill-labs/windmill/commit/4dbf8737238ccc4dc2c67365e6d43f04f46c75b5))
* persist on-behalf-of user across app deploy paths ([#9773](https://github.com/windmill-labs/windmill/issues/9773)) ([f99781c](https://github.com/windmill-labs/windmill/commit/f99781ca5f77248206c951935cc44acfa5f072eb))
* reject symlink traversal in job-dir path validation ([#9713](https://github.com/windmill-labs/windmill/issues/9713)) ([b5bd824](https://github.com/windmill-labs/windmill/commit/b5bd8245d81b84fc14d3ea955bf1e66ac576bf37))
* **monitor:** vacuum job_perms/job_result_stream right after each orphan sweep ([#9753](https://github.com/windmill-labs/windmill/issues/9753)) ([8912e21](https://github.com/windmill-labs/windmill/commit/8912e21d1571e57b5cf21b7d4d9520e20a28e70d))
* add resource and infrastructure telemetry ([#9737](https://github.com/windmill-labs/windmill/issues/9737)) ([9793d01](https://github.com/windmill-labs/windmill/commit/9793d01575415963a89609a1baf2cd64f0d050cc))
* render mermaid diagrams in chat code blocks ([#9738](https://github.com/windmill-labs/windmill/issues/9738)) ([cfb9f1d](https://github.com/windmill-labs/windmill/commit/cfb9f1dbc23110ecf8f91bb3c8c81fc6e35dc09b))
### Bug Fixes
* **ai-chat:** Fix incorrect editor edits from ai chat [#1](https://github.com/windmill-labs/windmill/issues/1) ([#9741](https://github.com/windmill-labs/windmill/issues/9741)) ([fc797a3](https://github.com/windmill-labs/windmill/commit/fc797a35fe7885630c81453df0fc94769e73873a))
* allow object storage test for non-super-admins, harden on cloud ([#9739](https://github.com/windmill-labs/windmill/issues/9739)) ([24446e8](https://github.com/windmill-labs/windmill/commit/24446e80093ade349f7fbf65063d2d1cb5551c1e))
* **frontend:** debounce external code→Monaco sync in Editor ([#9743](https://github.com/windmill-labs/windmill/issues/9743)) ([29c67ce](https://github.com/windmill-labs/windmill/commit/29c67ced97bf2919584986f9d9eceb4337c34ad9))
* pipeline annotation false-positives from body comments ([#9736](https://github.com/windmill-labs/windmill/issues/9736)) ([984ea72](https://github.com/windmill-labs/windmill/commit/984ea728d98649b66b1cae899bdab9af3176caa7))
* preserve fork parent linkage on workspace id change ([#9716](https://github.com/windmill-labs/windmill/issues/9716)) ([cbf54d4](https://github.com/windmill-labs/windmill/commit/cbf54d4eb432638e27f67c4c8b879cbcc0291da3))
* prevent variable push from corrupting is_secret variables ([#9705](https://github.com/windmill-labs/windmill/issues/9705)) ([ba4b368](https://github.com/windmill-labs/windmill/commit/ba4b368706e95e22f346a10e5fe145b0795ac3f6))
### Performance Improvements
* **monitor:** skip protected prefix in retention delete via cross-batch watermark (WIN-2088) ([#9744](https://github.com/windmill-labs/windmill/issues/9744)) ([e90b2be](https://github.com/windmill-labs/windmill/commit/e90b2be8fade1eb78cd685890291f5a4553a6a10))
This folder contains black-box benchmark cases for:
Black-box benchmark cases for the Windmill AI generation modes (`flow`, `app`,
`script`, `cli`, `global`).
-`flow`
-`app`
-`script`
-`cli`
-`global`
**Authoring and running cases is documented in the `ai-evals` skill** — load it
before adding/changing a case or running a benchmark. Claude Code reads
`.claude/skills/ai-evals/SKILL.md`; Codex and Pi read
`.agents/skills/ai-evals/SKILL.md` (same canonical file). Invoke with `/ai-evals` in
Claude Code, `$ai-evals` in Codex, or `pi --skill ai-evals`.
The goal is to test the current production prompts and guidance with realistic user requests, not to test one exact implementation shape.
For AI chat / copilot changes that these evals measure, see the `ai-chat` skill.
## Core rules
1. Write prompts like a real user request.
2. Prefer behavior, inputs, constraints, and outcomes over internal implementation details.
3. Keep deterministic validation narrow and hard.
4. Put semantic expectations in `judgeChecklist`.
5. Use `expected` fixtures only when exact structure really matters.
## Prompt writing
Prompts should sound like something a user would naturally ask.
Good:
- "Create a flow that routes support requests based on customer tier."
- "Add a reset button that sets the counter back to 0."
- "Create a flow that reuses the existing greeting script instead of duplicating the logic."
Bad:
- "Use `branchone` with 3 branches and a default branch."
- "Create a `rawscript` step with this exact topology."
- "This is a benchmark harness."
Do not write prompts as if the user knows Windmill internals unless the case is explicitly testing a power-user workflow.
## Flow-specific rules
This is the main principle you asked for:
- flow prompts should read like requests from a user who does not know the product internals
- the user should ask for behavior, not for `branchone`, `branchall`, `rawscript`, `preprocessor_module`, `failure_module`, exact graph topology, or other internal constructs
That means:
- creation cases should describe the business behavior and expected result
- modification cases may mention existing step names, because the user can see the current flow
- only mention special Windmill constructs when the case is explicitly about those constructs
Examples:
- acceptable creation prompt:
"Create a purchase approval flow that pauses for approval and asks the approver for a comment."
- avoid:
"Create a suspend step with one required event and a resume form."
For flow cases, do not fail a case just because the model chose a different valid topology.
## App-specific rules
App prompts should focus on user-visible behavior:
- what the UI should let the user do
- what should persist
- what backend behavior is needed
Avoid prompting in terms of React structure, component names, or implementation unless the case is specifically about editing an existing app.
## CLI-specific rules
CLI prompts can be more explicit about paths and file names because real CLI users often do specify them.
Still, avoid benchmark phrasing. The prompt should read like a repo task, not a harness instruction.
When relevant, ask the assistant to tell the user which `wmill` commands to run next. That is part of the benchmarked behavior.
## Global-specific rules
Global prompts should exercise workspace-level drafting behavior:
- inspecting existing scripts, flows, apps, schedules, triggers, resources, and variables when relevant
- writing AI drafts rather than saving or deploying by default
- producing coherent multi-artifact changes when the request crosses artifact boundaries
Keep deterministic validation focused on the draft contract: required draft type/path, required content snippets, forbidden draft paths, and forbidden mutating tools such as deploy/delete unless the case explicitly asks for them.
Datatable cases should set `skipJudge: true` and validate through tool-use
(`requiredToolsUsed` / `forbiddenToolsUsed`) and SQL-argument assertions
(`toolCallArgs` with `stringIncludesAnyOf`, e.g. `['select']`, `['create table']`,
`['update', 'insert into']`). Two reasons the judge is unreliable here:
-`list_datatables`, `get_datatable_table_schema`, and `exec_datatable_sql`
produce no drafts, and the global judge only sees the drafts artifact — it
scores a no-draft conversational answer as empty (same as the
`askUserQuestion` cases).
- Even a case that *does* produce a draft (a script reading the data table via
`wmill.datatable()` at runtime) is mis-judged: the judge has no datatable SDK
reference and penalizes correct `wmill.datatable()` usage as wrong. Verify the
- "the flow includes an approval step named `request_approval`"
- "`request_approval` pauses the flow and asks the approver for a comment"
- "`request_approval` is a real script step that generates approval/resume URLs (e.g. via `getResumeUrls`) so approvers receive an actionable link, not a no-op passthrough (identity) step"
- one approval is enough to continue
- "the flow includes a final step named `finalize_purchase`"
- "`finalize_purchase` returns an approved status object after approval"
- builds a data pipeline node as a script (not a flow)
- marks the script as a pipeline member with the pipeline annotation in the script's comment syntax (`-- pipeline` for a DuckDB/SQL node, not `// pipeline`)
- declares a schedule trigger and writes its output to a managed DuckLake table
- leaves the result as an AI draft and does not deploy or save it
- id:global-test-pipeline-two-node-chain
prompt:|-
Build a small data pipeline in the `f/evals/global` folder: one step that
ingests orders into a DuckLake table, and a second step that reads that table
and writes a daily order-count rollup table. Wire the second step to run off
the first step's output. Keep everything as drafts — don't deploy.
"query":"UPDATE materialized_asset_schema\n SET snapshot_id = $5, job_id = $6, captured_at = now()\n WHERE workspace_id = $1 AND asset_kind = $2 AND asset_path = $3\n AND version = $4",
"query":"SELECT workspace_id as \"workspace_id!\", MIN(completed_at) as oldest\n FROM v2_job_completed\n WHERE workspace_id = ANY($1::text[])\n GROUP BY workspace_id",
"query":"SELECT\n (SELECT MIN(completed_at) FROM v2_job_completed) as true_oldest,\n (SELECT MIN(completed_at) FROM v2_job_completed\n WHERE workspace_id <> ALL($1::text[])) as global_oldest,\n (SELECT COUNT(*) FROM v2_job_completed) as total",
"query":"SELECT DISTINCT ON (mp.asset_kind, mp.asset_path)\n mp.asset_kind AS \"asset_kind: AssetKind\", mp.asset_path,\n mp.snapshot_id AS \"snapshot_id!\", mp.partition\n FROM materialized_partition mp\n JOIN unnest($2::ASSET_KIND[], $3::text[]) AS u(kind, path)\n ON mp.asset_kind = u.kind AND mp.asset_path = u.path\n WHERE mp.workspace_id = $1\n AND mp.status = 'materialized' AND mp.snapshot_id IS NOT NULL\n ORDER BY mp.asset_kind, mp.asset_path, mp.snapshot_id DESC",
"query":"SELECT\n schedule.path, t.jobs FROM schedule,\n LATERAL(SELECT ARRAY(\n SELECT json_build_object('id', id, 'success', status = 'success', 'duration_ms', duration_ms)\n FROM v2_job_completed c JOIN v2_job j USING (id)\n WHERE trigger_kind = 'schedule'\n AND trigger = schedule.path\n AND c.workspace_id = $1\n AND j.workspace_id = $1\n AND parent_job IS NULL AND runnable_path = schedule.script_path\n AND status <> 'skipped'\n ORDER BY completed_at DESC\n LIMIT 20\n ) AS jobs) t\n WHERE workspace_id = $1 AND NOT starts_with(schedule.path, $4)\n ORDER BY edited_at DESC\n LIMIT $2 OFFSET $3",
"query":"\n SELECT language AS \"language!: _\", COUNT(*)::BIGINT AS \"count!\"\n FROM script\n WHERE archived = false AND deleted = false AND kind = 'script'\n AND (auto_kind IS NULL OR auto_kind <> 'wac')\n GROUP BY language\n ",
"query":"\n WITH completed AS (\n INSERT INTO v2_job_completed\n (workspace_id, id, started_at, duration_ms, result,\n flow_status, workflow_as_code_status, status, worker)\n SELECT\n q.workspace_id, q.id, q.started_at,\n (EXTRACT('epoch' FROM now()) - EXTRACT('epoch' FROM COALESCE(q.started_at, now()))) * 1000,\n CASE WHEN q.running\n THEN $3::text::jsonb\n ELSE $4::text::jsonb\n END,\n s.flow_status,\n s.workflow_as_code_status,\n 'skipped'::job_status,\n q.worker\n FROM v2_job_queue q\n LEFT JOIN v2_job_status s ON s.id = q.id\n WHERE q.id = $1\n ON CONFLICT (id) DO UPDATE SET status = EXCLUDED.status, result = EXCLUDED.result\n RETURNING 1 AS x\n ), _deleted AS (\n DELETE FROM v2_job_queue WHERE id = $1\n ), _logged AS (\n INSERT INTO job_logs (logs, job_id, workspace_id)\n VALUES ($5, $1, $2)\n ON CONFLICT (job_id) DO UPDATE SET logs = concat(job_logs.logs, EXCLUDED.logs)\n )\n SELECT x FROM completed\n ",
"query":"DELETE FROM v2_job_completed\n WHERE id IN (\n SELECT jc.id FROM v2_job_completed jc\n LEFT JOIN v2_job j ON j.id = jc.id\n WHERE jc.workspace_id = $5\n AND jc.completed_at <= now() - ($1::bigint::text || ' s')::interval\n AND jc.completed_at >= COALESCE($4::timestamptz, '-infinity'::timestamptz)\n AND COALESCE(j.root_job, j.flow_innermost_root_job, jc.id) NOT IN (\n SELECT u FROM unnest($3::uuid[]) AS u WHERE u IS NOT NULL\n )\n ORDER BY jc.completed_at ASC\n LIMIT $2\n FOR UPDATE OF jc SKIP LOCKED\n )\n RETURNING id, completed_at",
"query":"SELECT consumer_path AS \"consumer_path!\", macro_name AS \"macro_name!\"\n FROM macro_usage\n WHERE workspace_id = $1\n AND ($2::text IS NULL OR consumer_path LIKE $2)",
"query":"\n SELECT\n kind AS \"kind!: AssetKind\",\n path AS \"path!\"\n FROM asset\n WHERE workspace_id = $1\n AND usage_kind = 'script'\n AND usage_path = $2\n AND usage_access_type IN ('w', 'rw')\n ",
"query":"SELECT version, columns AS \"columns: Json<Vec<SchemaColumn>>\"\n FROM materialized_asset_schema\n WHERE workspace_id = $1 AND asset_kind = $2 AND asset_path = $3\n ORDER BY version DESC\n LIMIT 1",
"query":"\n WITH RECURSIVE chain AS (\n SELECT id, parent_workspace_id, 0 AS depth\n FROM workspace WHERE id = $1\n UNION ALL\n SELECT w.id, w.parent_workspace_id, chain.depth + 1\n FROM workspace w\n JOIN chain ON w.id = chain.parent_workspace_id\n WHERE chain.depth < 20\n )\n SELECT COALESCE(MAX(depth), 0)::bigint AS \"depth!\" FROM chain\n ",
"query":"INSERT INTO workspace (id, name, owner, deleted, premium, parent_workspace_id, is_dev_workspace)\n SELECT $1, $2, owner, false, premium,\n CASE WHEN $4 THEN parent_workspace_id ELSE NULL END, $5\n FROM workspace WHERE id = $3",
"query":"SELECT version, columns AS \"columns: Json<Vec<SchemaColumn>>\",\n snapshot_id, job_id, captured_at\n FROM materialized_asset_schema\n WHERE workspace_id = $1 AND asset_kind = $2 AND asset_path = $3\n ORDER BY version DESC",
"query":"\n WITH RECURSIVE tree AS (\n SELECT id, deleted, 0 AS depth FROM workspace WHERE id = $1\n UNION ALL\n SELECT w.id, w.deleted, tree.depth + 1 FROM workspace w\n JOIN tree ON w.parent_workspace_id = tree.id\n WHERE tree.depth < 20\n )\n SELECT COALESCE(MAX(depth) FILTER (WHERE NOT deleted), 0)::bigint AS \"height!\" FROM tree\n ",
"query":"SELECT path, kind, ahead, behind, has_changes, exists_in_source, exists_in_fork FROM workspace_diff\n WHERE source_workspace_id = $1 AND fork_workspace_id = $2",
"query":"SELECT path, kind, ahead, behind, has_changes, exists_in_source, exists_in_fork FROM workspace_diff\n WHERE source_workspace_id = $1 AND fork_workspace_id = $2\n AND NOT EXISTS (\n SELECT 1 FROM ws_specific ws\n WHERE ws.path = workspace_diff.path\n AND ws.item_kind = workspace_diff.kind\n AND ws.workspace_id IN (workspace_diff.source_workspace_id, workspace_diff.fork_workspace_id)\n )",
"query":"INSERT INTO ws_specific (workspace_id, item_kind, path)\n SELECT $1::varchar, 'resource', $2::varchar\n WHERE EXISTS (SELECT 1 FROM resource WHERE workspace_id = $1::varchar AND path = $2::varchar)\n ON CONFLICT DO NOTHING",
"query":"SELECT DISTINCT path AS \"asset_path!\", usage_path AS \"producer_path!\"\n FROM asset\n WHERE workspace_id = $1 AND kind = 'ducklake' AND path = ANY($2)\n AND usage_kind = 'script' AND usage_access_type IN ('w', 'rw')",
"query":"SELECT DISTINCT trigger_ref AS \"trigger_ref!\"\n FROM script_trigger\n WHERE workspace_id = $1\n AND runnable_path = $2\n AND trigger_kind = 'asset'\n AND runnable_kind = 'script'\n ORDER BY trigger_ref",
"query":"DELETE FROM macro_usage WHERE workspace_id = $1 AND (consumer_path = $2 OR macro_name IN (SELECT name FROM macro_definition WHERE workspace_id = $1 AND provider_path = $2))",
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.