* refactor: make the app policy's principal the authority for its identity
* fix: align the app backfill with the sibling migration and audit the uncached address
* chore: refresh the sqlx cache after rebasing onto the merged base
* fix: resolve the app execution address uncached, it decides the job's authorization
* chore: cache the EE queries at the ref this branch pins
* chore: cache the EE queries at the ref this branch pins
* fix: derive the app draft's on-behalf-of address on read
* chore: cache the query the draft derivation test added
* fix: derive the app identity on the draft-table and version reads too
* docs: state the draft resolver's authorization contract
* fix: resolve a draft's principal against workspace membership only
* chore: cache the membership lookup the draft resolver added
* fix: drop an unresolvable draft's address instead of leaving it stale
* perf: evict the address cache on change so app dispatch can read it
* fix: evict on superadmin role changes, not only address changes
* refactor: make the app policy's address optional instead of derived on read
* fix: follow an external superadmin's rename into the apps that name them
* docs: state the removal gate once, and correctly
* refactor: drop the app-policy version constant that gated nothing
* docs: drop the last reference to the removed constant
* perf: read the address cache everywhere now that eviction reaches every replica
* fix: keep persisted addresses off the cache the poller evicts asynchronously
* docs: state where the cached address is accepted and where it is not
* docs: keep the cache rule in one place and drop the stale premise
* docs: sort the two lookups by how long a wrong answer lives
* fix: resolve the schedule address uncached where it is written to the row
* docs: name the release this actually ships in
* perf: evict a superadmin's key per workspace instead of the whole cache
* fix: evict every alias a superadmin principal can be spelled as
* docs: describe the trigger as it is
* docs: cover the round-tripped read in the cache rule
* docs: record why a stale dispatch address cannot escalate
* fix: validate a dispatch address against the principal's live binding
* fix: carry the validated address through to the job row and token
* fix: record the validated address on the job row, not the one handed in
* test: run the substep tag check as the non-superadmin it means to test
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* fix: rewrite a stored app address that disagrees with its principal
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* docs: record the accepted staleness window of the cached dispatch address
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* fix: record the validated address on the job's audit row
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* docs: record the accepted rename race of pre-transaction identity resolution
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* docs: separate the app's stored address from the derived one in the resolver doc
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* docs: describe the job identity fast path the push comments skipped
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* fix: backfill a legacy group-prefixed username as the group it names
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* fix: resolve a schedule edit's identity before opening its transaction
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* fix: never resolve a disabled member to a same-named superadmin
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* docs: state what the email-change notify buys, and rewrap two comment lines
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* fix: keep a group's runnables when offboarding a legacy group-prefixed member
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* fix: read the app author from the stored address, as execution does
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* docs: record the rename race's full consequence as a known, accepted limitation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
* docs: record the keep-target group address case as a known, accepted limitation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JY4bBCR1q2c5XB8s2r7Ysc
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: show when the last git auto-pull status was recorded
* chore: bump ee-repo-ref for the auto-pull status fix
* fix: show the git auto-pull status age with TimeAgo instead of a year-less date
* test: pin that a stale auto-pull recovery cannot overwrite a newer state
* chore: bump ee-repo-ref for the conditional auto-pull recovery
* fix: keep TimeAgo counting past the first hour in noSeconds mode
* chore: bump ee-repo-ref for the clear_auto_pull_failure contract note
* fix: guard TimeAgo's boundary scheduler against invalid dates and pin same-head newer failures
* chore: bump ee-repo-ref for the timestamp-guarded auto-pull recovery
* test: cover a same-second newer failure surviving a stale auto-pull recovery
* chore: bump ee-repo-ref for the whole-failure recovery match
* test: name the recovery helper after its input, not its staleness
* chore: update ee-repo-ref to c6df9fdd9826efb40d3586a9f97d17dee98ac6ef
This commit updates the EE repository reference after PR #793 was merged in windmill-ee-private.
Previous ee-repo-ref: 6aff80b80cae4944a4a78a6b9244019bc37f368b
New ee-repo-ref: c6df9fdd9826efb40d3586a9f97d17dee98ac6ef
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix: bring back Publish to Hub for scripts
Publishing to the Hub moved to the folder-level flow, which publishes a
whole project and needs a workspace admin. That left no way to share a
single script, which is what private hubs mostly use the Hub for.
Restore the "Publish to Hub" item on the script detail page and in the
script list row menu. Both open the Hub's script submission form prefilled
with the script, on whichever Hub the instance is configured to use, and
are hidden when the instance disables the Hub. Flows and apps still reach
the Hub only inside a project.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: open the Hub tab before fetching, and hide Publish to Hub from operators
The script list row has to fetch the script before it can build the Hub
URL, and Safari refuses window.open after an await, so the tab never
opened there. Claim it inside the click with claimTab(), point it at the
Hub once the script loads, and close it with a toast if the fetch fails.
A blocked popup falls back to a late window.open, and says so if that is
blocked too.
Operators can't write scripts, so the row menu now hides the item from
them, as the script page's menu already does. The script page opens the
Hub with noopener, and scriptToHubUrl takes the script instead of eight
positional arguments.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* [ee] fix: accept any hub version of the git sync script in the token check
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sm7h47G1rC3qYADfkCtTiZ
* chore: update ee-repo-ref to c9b043f2860fdae150c8c4bf03f3ec98b7f300e5
This commit updates the EE repository reference after PR #792 was merged in windmill-ee-private.
Previous ee-repo-ref: 7815dafb68d34ec5fbeb0645a095fbb6eae8d4a0
New ee-repo-ref: c9b043f2860fdae150c8c4bf03f3ec98b7f300e5
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix: keep each dev server's session when worktrees share a host
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: keep dev auth cookies host-only on non-localhost hosts
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: keep the shared dev auth cookie with ISOLATE_DEV_AUTH=0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(ai-sessions): turn skills on by default, and group them by folder
A skill is instructions the workspace wrote for the assistant to use, so what
carrying one costs is context rather than access. Selecting each one before it
applied made publishing a skill a two-step affair, and left most of them unused.
Skills now default to on. No storage is rewritten to get there: the preference
keeps its key and holds a decision per path, so the older array of enabled paths
still reads as "these were on" and only the paths nobody decided about move. MCP
servers stay opt-in through the same factory — their tools reach an external
system, which is a different question from context.
The Skills settings list groups into a tree once skills span more than one
folder, with a switch per folder acting on everything beneath it, and the list
answers the keyboard: Up/Down walk it, Left/Right fold, Space flips the switch
under the highlight, Enter opens the skill.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija
* fix: give a modal the option to stand only as tall as the window
`AIPromptsModal` asks for 1000px of height, which is taller than a laptop
window: the dialog then scrolled inside the overlay while its list scrolled
inside the dialog — two scrollbars, one of them moving the modal itself. The
cap `Modal2` appeared to have, `max-h-screen-80`, is defined nowhere in the
tailwind config, so it never applied to anything.
`fixedHeight="viewport"` is a new value that stands as tall as the window
allows. Deliberately a definite height rather than a max-height: bodies here
size against the box with `h-full` / `grow min-h-0` and scroll inside it, and a
max-height leaves them nothing to resolve against — they grow past the surface
instead. Every existing size keeps the height it has today, so no other modal
moves. The two classes that resolved to nothing are removed.
The prompts modal and the assistant settings modal take the new value; both
already scroll inside themselves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija
* fix: pin read_skill's gate in its test, and say who a delete affects
The refusal `read_skill` gives for a path that is not a skill changed shape —
it checks the workspace listing now, not just the off-switch — and its test was
still asserting the old wording against an unmocked listing.
The delete confirmation said everyone "who selected it" loses the skill, which
stopped being true when skills started defaulting to on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija
* fix: keep the keyboard walk when the list scrolls under the pointer
The mouse takes the skills list back on a real movement over it, not on
`mouseenter`. The browser fires that one whenever rows arrive under a
stationary pointer — every scroll the keyboard itself causes, and every folder
collapse — so walking Down past the bottom of the list handed control back to a
mouse nobody had touched, and the next press restarted at the top.
Also from the review round: the "+" menu sorted skills on-first, a key that is
constant now that they start on, and pushed the one row it did move — a skill
just turned off there — out of the shortcut that turns it back on. It orders by
path. The remaining "selection" wording follows the vocabulary the rest of this
change moved to.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija
* fix: carry the keyboard walk on from the row the mouse left it on
Handing the list to the mouse dropped the highlight, so the next arrow press
started again at the top. It moves to the row under the pointer instead —
invisible while the mouse leads, since drawing and acting both wait on the
keyboard being in charge, and exactly where someone would expect the walk to
carry on from.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija
* fix: cap the AI prompts modal from its own call site
Reverts `Modal2` and the assistant settings modal to what they were. The
prompts modal asks for `xxl`, 1000px, which is taller than a laptop window, so
the dialog scrolled inside the overlay while its list scrolled inside the
dialog. It now passes `max-h-[80vh]` through the `css.popup` the component
already forwards.
The height stays definite underneath, which is what lets the list bound its own
scroller, and nothing outside this one modal changes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija
* refactor: drive the skills list highlight with useListHighlight
The Tools section next door already had this: `useListHighlight` owns the
highlighted index, wrapping, `scrollIntoView`, and the rule that a scroll under
a resting pointer must not hand the list back to the mouse — the bug this
section rediscovered the hard way. Reusing it drops the parallel implementation.
What stays local is what is actually a tree: Left and Right fold a folder or
step into it, Space flips the switch under the highlight, and Enter opens the
lit skill. `restingIndex` is what keeps the highlight on a folder through a
fold, where a search would instead send it back to its top hit.
The keys are answered at the window rather than on the list: leaving the editor
parks focus elsewhere, and a container-scoped handler goes silent when it does.
`move` is now returned by the composable, for the step into a folder's children.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija
* fix: stop the fold's sticky row resetting the keyboard walk
`stickyKey` is read through `restingIndex`, which `useListHighlight` calls
inside the effect that reacts to the row count. As `$state` it was also a
dependency of that effect, so clearing it on the next arrow re-ran the effect
and wrote the highlight back to nothing: after collapsing a folder, one Down
lit nothing and the one after it started again at the top.
It is a plain variable now, read when the effect runs and invalidating nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija
* fix: keep one lit row, and keep the fold's sticky row to its fold
Three from the review of the `useListHighlight` swap:
The sticky row a fold takes is now given up as soon as that fold has rendered.
Held until the next arrow, it pulled the highlight back to that folder on any
later change — another fold, a save, a delete, a workspace switch.
Space and Enter on a focused control bring the highlight to that control's row
before the control answers them. A switch keeps focus after a plain click, and
the row drawn as highlighted was then a different one from the row that flipped.
Up and Down carry on from a row reached with Tab. `useListHighlight` cannot see
that by itself: `ListRow` puts the row's id on its outer div while focus sits on
the button inside it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija
* fix: land the highlight on a named row rather than stepping to it
`move` counts steps from wherever the highlight is, and from nothing lit it can
only reach an end of the list — so the three places that meant "put it on this
row" (a row reached with Tab, the row of a focused control, a folder's parent)
sent it to the first row whenever nothing was lit yet. `useListHighlight` grows
a `moveTo` for naming the row outright, and those three use it.
The handler's own doc still said the keys are answered on the list; they went
back to the window when the editor's page transition proved able to take focus
away from it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija
* fix: fold from the header click the way every other fold does
The header's own click wrote `collapsed` directly instead of going through
`fold`, so the row count changed with no row named to keep: the highlight reset,
and since the highlight is the header's only hover feedback, it went flat under
a pointer that had not moved and stayed flat.
Also from the round: a duplicated `svelte-ignore`, the missing one on the header
wrapper that takes `onmouseenter`, and a trailing comma prettier wanted gone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: keep pinned import versions of imported scripts in bun lockfiles
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test: guard pinned imports through an unlocked multi-file bun run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: let admins and background sync reach private git hosts
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Hb4vHnVrCMqe8ZtNtzFSs
* chore: point ee-repo-ref at the private git host change
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Hb4vHnVrCMqe8ZtNtzFSs
* fix: treat any admin token as admin and pin git probe transports
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Hb4vHnVrCMqe8ZtNtzFSs
* fix: pin git probe transports with a test and say what the caller check skips
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Hb4vHnVrCMqe8ZtNtzFSs
* chore: update ee-repo-ref to eccad9f68bd7246cc81acb82bdb6c08fc6013f45
This commit updates the EE repository reference after PR #791 was merged in windmill-ee-private.
Previous ee-repo-ref: 45ed1331a82dc15e6bdf15fd63517227f9160e21
New ee-repo-ref: eccad9f68bd7246cc81acb82bdb6c08fc6013f45
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix: attach TLS to gRPC OTLP exporters for https endpoints
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RbLzVfFDZ9pjBGHSzCSrNZ
* chore: bump ee-repo-ref for the gRPC TLS resolution test
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RbLzVfFDZ9pjBGHSzCSrNZ
* chore: update ee-repo-ref to afd59490e2ca3c375d64cf4a91041763c7de4766
This commit updates the EE repository reference after PR #790 was merged in windmill-ee-private.
Previous ee-repo-ref: b6dd68beb144ecf1b978172396ff8c8bb27ae0c2
New ee-repo-ref: afd59490e2ca3c375d64cf4a91041763c7de4766
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix(frontend): keep crawlers off the login page
Every page on the hub links to /user/login with itself in `rd`, so a crawler
sees one login URL per hub page — 4,311 of them in Search Console, all
rendering this same form and flagged as duplicates without a canonical. Nothing
about a login page belongs in an index, on any instance.
Mark the page noindex, as public_run already is, and ship a robots.txt that
keeps crawlers out of /user/ and /api/. The frontend is embedded as static
assets with an index.html fallback, which is why /robots.txt answered with the
app shell until now; a real file in static/ is served as itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): let crawlers fetch the login page so the noindex is seen
robots.txt disallowed /user/, which stopped a crawler fetching /user/login at
all — and a page that is never fetched never shows its noindex. The two halves
cancelled: the URLs would have moved from "duplicate" to "blocked" rather than
out of the index.
Drop the disallow, keeping /api/. And since the app is client-rendered, the
meta tag only exists after a render pass; send X-Robots-Tag on /user/* from
serve_path as well, which a crawler sees on the first fetch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix: surface why a private or untrusted git host is unreachable
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBriXeDGzjBWjSUTgpkCxW
* test: assert the private git host refusal names ALLOW_LOCAL_GIT_REMOTES
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBriXeDGzjBWjSUTgpkCxW
* test: pin that the url credential stays out of the refused-host error
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBriXeDGzjBWjSUTgpkCxW
* chore: update ee-repo-ref to fe2418ff4e5630d6ad3fd85cd2c865bf51c87a2a
This commit updates the EE repository reference after PR #789 was merged in windmill-ee-private.
Previous ee-repo-ref: af0f3ca96f2fbcfa4bf4f8498824c52001d72c55
New ee-repo-ref: fe2418ff4e5630d6ad3fd85cd2c865bf51c87a2a
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat(otel): honor OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iN8SNZQva5MfMnSM43fHq
* fix(otel): print exporter build failures that tracing would drop
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iN8SNZQva5MfMnSM43fHq
* chore: update ee-repo-ref to a8ce9bed4b16a01a964d4c35bdc3092b89d10495
This commit updates the EE repository reference after PR #788 was merged in windmill-ee-private.
Previous ee-repo-ref: 7445aa186ca599c3353e041986fe46a277ba9e9f
New ee-repo-ref: a8ce9bed4b16a01a964d4c35bdc3092b89d10495
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat: show the workspace an operator is in, and let them switch
Operators get a single burger button instead of the developer sidebar, so
nothing on screen named the workspace they were working in.
The button now carries the workspace colour as a disc behind the hamburger,
with the workspace name beside it — permanently on home, and on hover
elsewhere. Workspaces without a colour fall back to a neutral disc so the
button always reads as a control against the page behind it.
A "Switch workspace" submenu lists the operator's workspaces, reusing the
developer sidebar's picker: that list moved out of WorkspaceMenu into
WorkspacePickerBody so both render the same rows. "All workspaces" moved from
the operator menu into the bottom of that picker, outside its scroll area so it
stays reachable.
An operator switching workspaces lands on home, since their page access is
granted per workspace and the page they are on may not be theirs to open in the
one they switch into. Developers keep the existing stay-on-the-page behaviour,
as do pickers embedded in a page that drives its own navigation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmXFP3eaPPdZuVwVpLuYRX
* fix: decide the operator landing from positive role signals only
`operator_settings` is NULL both for a non-operator and for an operator whose
workspace never had settings written, so a `false` from `isOperatorInWorkspace`
could not stand as proof of a developer. Lead with `userStore.operator`, an
explicit flag for the workspace being left, and keep the target check only as a
confirmation. Also states the picker's expansion-seeding invariant as what the
host menu actually guarantees: a close and re-open inside its 100ms outro
resumes the same instance rather than a fresh one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmXFP3eaPPdZuVwVpLuYRX
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Selecting a step in the flow editor sometimes opened the Settings panel
instead of the step that was clicked.
SelectionManager.selectId() clears xyflow's own selection before setting
ours, and that clear was wired to clearFlowSelection(), which resets
offsetNodeCache and reassigns nodes. That hands xyflow node objects it
does not recognise, which is what makes it drop the flag — but it also
makes it re-create every node's DOM: 126 elements on a 25-node flow, on
every selection.
Graph nodes select on pointerdown. A click is dispatched on the closest
common ancestor of its pointerdown and pointerup targets, so when the
release lands in that teardown gap the browser hit-tests to the pane, the
click addresses the pane, and onpaneclick clears the selection back to the
settings sentinel.
Clear through store.unselectNodesAndEdges() instead, so no node object
changes identity and there is no gap to fall into. clearFlowSelection
keeps its two group-creation callers, which rebuild the graph anyway.
Claude-Session: https://claude.ai/code/session_01R7caEKonPr2DmvsWRR6bCK
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): restore heading sizes in note markdown and keep group notes on id change
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182GroR5mvi9ksRTMmBT7nE
* fix(frontend): scope the note cleanup deferral per note
Address local review nits: the header comment overstated the heading ramp (h3
sits at body size in the xs and sm scales), and the mid-update deferral bailed
out of cleanup for every group note rather than the one holding the unrendered
module.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182GroR5mvi9ksRTMmBT7nE
* perf(frontend): traverse modules without collecting a discarded array
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182GroR5mvi9ksRTMmBT7nE
* test(frontend): show the three markdown prose presets in the kitchen sink
The Markdown tab rendered only the default preset, so a change to the shared
heading scale could not be compared across the surfaces that use it. The chat
sample gains headings for the same reason: it is the only place the assistant
bubble renders at panel width without an AI provider.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182GroR5mvi9ksRTMmBT7nE
* refactor(frontend): validate group notes against the flow, not the render
A group note's members are module ids, so the flow's own module list decides
whether one still exists. Validating against the rendered nodes instead needed
a special case for collapsed groups, and still dropped a live module in the
pass after its id changed. Both cases are the same mistake, and checking the
source of truth removes them together with the collapsedModuleIds parameter.
Path completion keeps working off the rendered graph, since it needs the edges,
and now skips a note whose members it cannot all see rather than dropping them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182GroR5mvi9ksRTMmBT7nE
* fix(frontend): name a new note "Note", not after its internal type
The prefill lands inside the user's own note, and "Free note" / "Group note"
are the serialized type, not words anyone says about a note they just drew.
The menus that create them already say "Add note".
The heading goes to h2 as well: h3 computes to the body size in the note scale,
so the prefill's own title did not read as one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182GroR5mvi9ksRTMmBT7nE
* docs(frontend): describe what collapsedModuleIds is still for
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182GroR5mvi9ksRTMmBT7nE
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(otel): pick up standard OTEL_RESOURCE_ATTRIBUTES on the exported resource
The OTEL resource was built with `Resource::builder_empty()`, which runs no
resource detectors, so attributes injected through the standard
`OTEL_RESOURCE_ATTRIBUTES` env var were silently dropped. Deployments that
inject `k8s.pod.uid`, `k8s.container.name` or `service.namespace` saw none of
them reach their backend.
Use `Resource::builder()`, which seeds from the SDK's env detector. Windmill's
own attributes keep being applied on top, so per the OTel resource spec the env
var is the secondary resource and `service.name`, `service.version`,
`host.name` and `deployment.environment*` stay authoritative.
The EE change lives in windmill-ee-private; this carries the ee-repo-ref bump
and a regression test pinning both halves of the contract.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yUJaBDHPgjZPodP2u4aqR
* test(otel): clear OTEL_HOST_NAME so the resource test is hermetic
OTEL_HOST_NAME takes precedence over the hostname argument, so an ambient one
failed the host.name assertion with a message pointing at the merge logic.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yUJaBDHPgjZPodP2u4aqR
* feat(otel): honor OTEL_SERVICE_NAME and OTEL_SERVICE_VERSION
Deployments identify each pod from its own labels, e.g. through the Kubernetes
downward API, so `service.name` and `service.version` must be settable per pod.
Both were ignored: OTEL_SERVICE_NAME was read by the SDK and then overwritten,
and because the two attributes are set in code they also outrank
OTEL_RESOURCE_ATTRIBUTES, leaving no route to set them at all.
The EE change lives in windmill-ee-private; this carries the ee-repo-ref bump
and tests for the dedicated overrides.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yUJaBDHPgjZPodP2u4aqR
* fix(otel): keep service.version pinned to the build version
OTEL_SERVICE_VERSION is not an OTel env var, and service.version identifies the
build that produced the telemetry, which a deployment cannot state more
precisely than GIT_VERSION already does. A deployment that wants its own release
version in telemetry can carry it under its own key.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yUJaBDHPgjZPodP2u4aqR
* test(otel): pin OTEL_SERVICE_NAME above service.name in OTEL_RESOURCE_ATTRIBUTES
The spec ranks OTEL_SERVICE_NAME above a service.name carried in
OTEL_RESOURCE_ATTRIBUTES; that ordering was only checked by hand. The three
candidate values are distinct, so the assertions fail if either ranking breaks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yUJaBDHPgjZPodP2u4aqR
* feat(otel): honor OTEL_SERVICE_VERSION
The spec defines no OTEL_SERVICE_VERSION, but deployments set it expecting it to
work because it sits next to OTEL_SERVICE_NAME, and setting service.version in
code blocks the OTEL_RESOURCE_ATTRIBUTES route, so there is otherwise no way to
set it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yUJaBDHPgjZPodP2u4aqR
* test(otel): guard against unknown_service on a default deployment
Resource::builder seeds SdkProvidedResourceDetector, which sets service.name to
"unknown_service" when neither OTEL_SERVICE_NAME nor a service.name in
OTEL_RESOURCE_ATTRIBUTES is present. Only our own attribute keeps that out of
the exported resource, and no assertion covered the case where nothing is set
at all — which is the default deployment.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yUJaBDHPgjZPodP2u4aqR
* test(otel): pin the empty-means-unset fallback for OTEL_SERVICE_VERSION
The empty case asserted the fallback for service.name and host.name but not
service.version, leaving one branch of the three-variable contract uncovered.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yUJaBDHPgjZPodP2u4aqR
* chore: update ee-repo-ref to b964f0caaae57dc526c7ac9dc54d753372989f63
This commit updates the EE repository reference after PR #779 was merged in windmill-ee-private.
Previous ee-repo-ref: 62efa909aabdba4cb31ffabe9aae0e4909ca1e07
New ee-repo-ref: b964f0caaae57dc526c7ac9dc54d753372989f63
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix(python): stop uv pip compile emitting lockfile annotations
`uv pip compile` annotates each resolved package with the indented `# via <pkg>`
comments that name what pulled it in. Windmill installs a lockfile one entry at
a time as a `uv pip install` argument, so those lines are unparseable package
names to the reader, and the compile step was stripping them back out of its own
output.
Pass `--no-annotate` so they are never written in the first place: the stored
lockfile is annotation-free at the source rather than by virtue of the reader
filtering them.
Lockfiles that arrive already annotated (deployed from outside Windmill) are
handled by `requirement_from_lockfile_line` (#11035); this only covers the ones
Windmill generates itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: state what the annotation filter below cannot catch
The comment justified the flag against the previous design instead of recording
the constraint that keeps it there. uv's annotation style is configurable, and
`[pip] annotation-style = "line"` in the worker HOME's uv.toml emits annotations
inline, which the whole-line `#` filter on the output does not touch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: disable a dynamic input when its schema field is disabled
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh
* refactor: extract the run form's argument hygiene into job_args
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh
* feat: give Tabs an opt-in sliding selection indicator
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh
* refactor: share the chat's scroll-fade measurement
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh
* feat: give the chat a run-form contract and incremental job output
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh
* feat: run and test a script from the chat through an argument form
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh
* feat: carry a chat run's card and job across saves and reloads
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh
* feat: render a chat run as a tool call row with its form, logs and result
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh
* feat: open a pending run form in the sessions preview pane
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh
* test: benchmark running a deployed script from the chat
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh
* feat: offer a test run's dynamic options from the draft it previews
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: say that a test run's dynselect helper executes on form display
* fix: send a schema default the model omitted when yolo skips the form
* fix: infer a test run's schema when the stored one declares no properties
* docs: tighten the note on the form's mount-time helper job
* fix: apply a nested schema default the bypass posture counts as answered
* fix: apply a declared default to a null value and an optional nested field
* fix: check required fields inside a supplied optional object before bypassing
* fix: read required args as own properties before bypassing the form
* fix: stop the turn from the run form's action row in the preview panel
* refactor: drop the run-form prediction and share its secret minting
* refactor: prefill a proposed secret instead of emptying the field
* docs: correct the comments the run-form prediction left behind
* fix: keep a proposed secret out of the chat's stored messages
* docs: say what a literal secret argument now does
* test: restore the copilotInfo export the aiStore mock omits
* docs: cut the run form's helper-script note to its constraints
* refactor: settle a run form from one entry and fetch a job's logs once
* fix: separate colliding secret paths, gate plan mode, keep polled logs
* fix: mint before the form opens, skip empty fields, show what ran
* revert: mint a run form's secrets at submit, not before it opens
* fix: settle a cancelled run card on the form's arguments, not the proposal
* fix: settle a stopped run form like a cancelled one, and keep an empty secret empty
* fix: snapshot a run's arguments before minting its secrets
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The public read-only run view had no way out. A team member who lands on a
shared link had to rebuild the /run URL by hand to retry, cancel or edit the
job. Add an "Open full view" link in the header bar, pointing at
/run/{id}?workspace={workspace} in the same tab; signed-out visitors get the
login page with a redirect back to the run.
Claude-Session: https://claude.ai/code/session_019xBcJ5EB5uhwqWysHbG4w6
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* [ee] feat: create a personal workspace on cloud signup instead of the demo invite
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* feat(frontend): land cloud users in their workspace after onboarding
Cloud signup creates exactly one workspace for the new user, so the picker
that followed onboarding was a page with a single choice on it. Switch to
that workspace and go to the home page instead, falling back to the picker
whenever there is a real choice: an invite to accept, several workspaces,
or none.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* chore(frontend): remove the tutorial system
Deletes the guided-tour feature: the tutorials directory, the per-editor
wrappers, the home banner and button, the /tutorials route, and the
driver.js dependency they were built on. Also removes what only existed to
serve them — the `tutorialsToDo` / `skippedAll` / `isCurrentlyInTutorial`
stores, the `disableTutorials` prop chain through the flow editor, the
`?tutorial=` deep links, PopupV2's clickOutside exemption for the driver
popover, and the selector-anchor class on the flow editor tabs.
The backend `tutorial_progress` endpoints and table stay: nothing calls
them now, and removing them is a public-API break plus a migration that
would drop existing progress.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* feat(frontend): suggest Hub projects on an empty workspace home
A workspace with nothing in it showed only "Welcome to Windmill". Replace
that with a grid of ready-made Hub projects to import, and hide the search
box, kind toggles and the sort/filter row while the workspace is empty —
they would act on an empty list. A search that matches nothing still keeps
its controls and shows the no-match message.
The project list is seeded locally for now; the Hub endpoint that ranks
them is not there yet, and Import is still a placeholder.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* feat(frontend): make a new cloud workspace the thing onboarding produces
Signup already makes a personal workspace; nothing let its owner name it, and a
user who ended up without one landed on a workspace picker whose only action was
a button. Onboarding now ends on the workspace itself, and the empty home that
follows says what a workspace is for rather than "Welcome to Windmill".
Onboarding gains a third step that names the workspace signup created, prefilled
from the login provider's name or the email local part — `ruben@…` gives "Ruben's
workspace". Skipping the survey reaches it too: the questions are ours, the
workspace is theirs. Advanced settings swaps in the real creation form for
someone setting up for a team.
The workspace picker stands down when it has nothing to offer: no workspace to
enter and no invite to accept leaves one action on the page, so the page is that
action — one field, prefilled, "Create workspace". Both hand-overs hold a loading
state for 900ms and the app fades in behind them, so creating a workspace reads
as something that happened.
The empty home draws three static placeholder rows in the shape of real ones,
under a caption offering a template or the New menu. "Start from a template"
opens a popover listing the hub's projects, most-starred first, preloaded when
the empty state renders and paged as you scroll. Picking one opens the import
wizard in a dialog: its two destination steps are already answered by being in a
workspace, so it starts at the import itself and pages to the credentials step
with the animation the paged-modal pattern provides.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): gate the empty state and repair the derived workspace id
Review findings from the first round.
The empty state offers a template import and the New menu, and neither checks a
permission — so an operator, or a workspace whose direct-deploy protection
cleared `showEditButtons`, was offered both. It now sits behind the same gate as
the create menu thirty lines above it.
`validateWorkspaceId` answers with the *reason* an id is unusable, so
`if (validateWorkspaceId(next)) break` stopped on the first invalid candidate and
returned it: someone named Global got the reserved `global`, and a 50-char seed
got a taken one. Invalid candidates are skipped instead, and when none works the
caller opens advanced settings rather than posting a name the server refuses.
Also: `rd` may be absolute (the CLI login sends one) and `goto` refuses those,
which would strand the caller on the "Creating …" screen with the workspace
already made; the hub host is parsed defensively, since the instance setting is
whatever an admin typed and `new URL` was throwing in render; `insert_workspace`
says which authorization its callers still own; and the two arrival animations'
comments now describe when they actually play.
Tests for the two pure helpers the review named: `defaultWorkspaceName` and
`hubProjectDescription`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): use unifiedSize on the new onboarding step's buttons
`size` and `color` are deprecated on Button; the new step copied them from the
survey steps above it. AGENTS.md: deprecated props survive at old call sites,
copying one forward is still a bug.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): let Skip wait for the workspace onboarding names
`loadWorkspaceStep()` was fired and dropped, while Skip and the use-case Continue
branch on `ownWorkspace` in their `finally`. Skip awaits one POST that starts
after those two GETs and can finish before them, so a first-frame Skip fell
through to `leaveOnboarding()` and landed in the workspace with the backend's
name — the step this flow exists for, silently gone. Both exits await the load;
`isSubmitting` already covers the wait.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* refactor: create the workspace in onboarding rather than at signup
Signup no longer makes a personal workspace, so the last onboarding step creates
one instead of renaming it — the same one-field `SimpleCreateWorkspace` the
workspace picker falls back to, so a user who leaves onboarding early meets the
form again rather than something new. The id now comes from the name they type
rather than from their email, and there is one creation path instead of two.
`insert_workspace` goes back to being private: the extraction existed only so the
EE signup path could call it, and nothing outside `create_workspace` does now.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): count a pending invite as somewhere to go
Onboarding read `listUserWorkspaces`, which returns membership. An invite is a
`workspace_invite` row until `accept_invite` runs, so an invited teammate reached
the last step owning nothing and was walked into creating a personal workspace,
with the invite nowhere on the page. Invites are fetched alongside the
workspaces, the way the picker already gates the same decision.
A failed load now reads as placed rather than not: the picker can work the
decision out, while the create step's only way forward is creating.
The create form reports when it is handing over, so the Previous button beside
it stands down for the ~900ms rather than offering a way back out of a workspace
that now exists.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): open the template picker downward and size it from the popover
The popover's default positioning caps its height to the viewport, and the list
inside it carried a fixed one, so a capped box overflowed its own frame — visible
with the AI composer hidden, where the caption sits high and `placement: top`
left almost no room above it. It opens downward now, with flip fallbacks, at a
definite `min(72vh, 520px)`; the list fills what the header leaves, which is
still the definite height it needs to page.
`creating` on the create form becomes `onCreatingChange`: `$bindable(default)` on
an optional prop is banned, and this is something the form reports rather than
state it shares.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* refactor(frontend): drop the InfiniteList containerClass prop
Added for the template picker, which turns out not to need it: DataTable's own
container is already `h-full`, so `containerClass="h-full"` merged to nothing and
the height the list pages against comes from the flex chain above it. A prop with
no effect at its only call site is public surface for free. InfiniteList is back
to what it was.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): stop a dismissed project import from running on
Modal reports dismissal only through its bindable `open` — the X, Escape
and the backdrop dispatch neither `confirmed` nor `canceled`. Bind it, so
clearing `pick` follows the dialog closing: re-picking the same project
opens it again, and a run still in flight is abandoned with a toast
instead of writing to the workspace with no UI in front of it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): stop a dismissed project import from running on
Modal reports dismissal only through its bindable `open` — the X, Escape
and the backdrop dispatch neither `confirmed` nor `canceled`. Bind it, so
clearing `pick` follows the dialog closing: re-picking the same project
opens it again, and a run still in flight is abandoned with a toast
instead of writing to the workspace with no UI in front of it.
Also mark the inline-link buttons as sanctioned rather than oversights,
and give Log out `text-accent` instead of `text-blue-500`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): keep the stopped-import toast off the Finish path
`done` survives a retry, so Finish is clickable while the run is going
again, and its own closing reaches the same falling edge the X does.
Abandon the run either way; say it was stopped only when that is what
the click asked for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* feat(frontend): reach the hub importer from the create menu, and count the funnel
The picker only existed inside the empty state, which disappears as soon
as a workspace holds one item — nothing else in the product linked to
`/projects/import`. New → Import now offers a hub project, opening the
catalogue in a dialog: a popover anchored to an item inside an open
dropdown leaves two melt layers arguing over focus. The list and the
import dialog move up to ItemsList, so one dialog serves both doors.
`template_setup` records how the credentials step ended — `filled` only
when nothing was outstanding, `skipped` carrying how many rows were left
— and `template_abandon` records where a dismissed import was given up.
`template_picker_open` gains a key naming the entry point.
Also on the workspace picker: logging out is a text link on the line
that says who you are and an item in the settings menu, rather than the
page's accent action, and onboarding's Previous joins the row it belongs
to instead of hanging under the button that finishes the form.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): make the import counters answer what they claim to
`template_abandon` folded a landed import closed with the X into `idle`,
the bucket read as "opened this and bounced" — three outcomes in one
number. It gets its own `done` stage.
`template_setup` counted skipped rows through `value`, which is an
increment: `skipped` accumulated rows while `filled` and `none` counted
imports, two units in one counter with no way to recover one from the
other. The row count becomes a bucket in the key, so every event is one
import and the buckets compare.
The import dialog also asked for the hub URL settings at init, and the
home list now mounts it for everyone on every arrival — two GETs for a
string only the project card renders. Deferred to the first pick.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix: sanitize the inferred username, and stop counting an unread setup step as clean
`loadUsernamePolicy` derived a username by stripping dots, so `O'Connor`
and `alice+demo` both produced values the `proper_username` constraint
refuses — posted invisibly by the simple form, which then failed with
nothing on screen explaining why. `usernameFromName` keeps only `[\w-]`
and answers undefined when nothing usable is left, which is already the
form's cue to open the full one.
The credentials step offers Finish when the export could not be read,
since it cannot tell what is outstanding — and that landed in
`template_setup` as `filled`, the bucket meaning the step came out
clean. It reports whether it checked anything, and an unread step counts
as `unchecked`.
Also drops an orphaned `.sqlx` entry left by the create-at-signup query
this branch abandoned, and rewrites the stepper's first-frame comment,
which argued from a meaning of `resourceCount` that main has narrowed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): cap the inferred username at what the column holds
`usr.username` is VARCHAR(50) while the provider name and email it is
derived from run to 255, and `create_workspace` inserts the value
untruncated — so a long first name failed the same way the invalid
characters did: posted invisibly, refused on insert, with nothing on
screen naming the field. Undefined instead, which the form already
routes to the full one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): show the import note where it applies, and drop onCreated's unused id
The note is about landing on top of what a workspace already holds, so
it belongs wherever the destination is an existing workspace. The route
already read it that way; the dialog, which always imports into the
current workspace, was hiding it. It costs one collapsed row.
`onCreated` was typed as taking the new workspace id, and the advanced
branch passed `''` because `CreateWorkspaceInner` does not report one.
No caller reads it — the form has already switched to the workspace by
then — so the argument goes rather than the lie staying.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): keep the empty-state toolbar reachable, and name the view counters
"Empty" here means the default listing found nothing, and a workspace
whose items are all archived looks exactly the same. The searchbar
carries "Only archived", so taking it off the pointer left those items
unreachable without hand-writing a query URL. Dimmed still, never
`inert`.
The disclosure named the counters that fire on a creation or an import
and not the three that fire on merely seeing the empty home or opening
either picker.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): respect disable_hub in both hub-project entry points
An instance with the hub turned off still got the catalogue preloaded on
every empty home and an "Import a hub project" entry in the create menu
— an outbound request the operator has said not to make, and a door to
somewhere unreachable. Both now observe `disableHubStore`, the store the
script and flow hub pickers already read. With the hub off the caption
reads "Create a new one." rather than continuing a sentence whose first
half is gone.
The telemetry disclosure also scoped the create menu and picker counters
to the empty home, when both fire from the toolbar in a populated one,
and said a creation was recorded when what is recorded is the menu
opening.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* refactor(frontend): drop the catalogue preload rather than gate it twice
Warming the hub catalogue when the empty state rendered bought the time
between the caption appearing and someone clicking it, and cost two
defects: the request fired on instances with the hub turned off, and the
gate added for that raced `disable_hub`'s own load, which starts false
and stays false if the settings request fails.
The picker fetches on open instead. Measured: nothing before the click,
one request after it, 411ms to a filled list. `disableHubStore` still
hides the link and the menu entry, which cost no request and correct
themselves if the setting lands late.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* docs: count the home actions, and state the lazy-fetch constraint without its history
The telemetry doc's tally is maintained by hand and main had just moved
it; this PR adds a feature, so it reads 48 across eighteen with `home`
in the list — verified against the pinned EE ref rather than counted by
eye.
The empty state's comment narrated a preload that no longer exists and
the defects it caused. What a future reader needs is the constraint:
`disable_hub` loads asynchronously, so a fetch from here goes out before
the setting forbidding it is known.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): stop the prefill overwriting a typed name, and gate submit on the policy
`load()` assigned the suggested name unconditionally, so a name typed
while its two requests were in flight was replaced a moment later. It
now yields to anything already typed.
Nothing may be submitted before the username policy lands either:
`automateUsername` starts at the common case, and posting that guess to
an instance that derives no usernames sends none where one is required.
`policyLoaded` gates both the button and `create()`, and is set in a
`finally` so a failed load leaves the form usable rather than wedged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): settle the username policy on failure instead of guessing it
`policyLoaded` was set in a `finally`, so a failed policy load unblocked
the form with `automateUsername` still at its default — the exact submit
the flag exists to prevent. The failure now hands over to the full form,
which asks for a username outright rather than inferring one, so the
flag is never true while the answer is still a guess.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): answer the username policy instead of rejecting it
Three rounds of this bug moved between call sites because the shared
loader rejects when it cannot read `automate_username_creation`, leaving
each caller to guess — and both guessed "automated", which hides the
username field and posts none to an instance that derives none.
`loadUsernamePolicy` now answers "ask for one" in that case, so
`SimpleCreateWorkspace` and `CreateWorkspaceInner` both render a field
someone can type into rather than submitting a guess. An instance that
does automate ignores a username it was sent, so asking is safe either
way.
The prefill and the policy are settled apart now too: a failed
`globalWhoami` costs the suggested name and nothing else.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): block creation when the username policy is unknown
There is no safe default. `create_workspace` refuses a username on an
instance that automates them and requires one on an instance that does
not (`workspaces.rs:5820`), so a client that cannot read the setting has
two request shapes available and the server rejects both. Last round's
"ask for one" was as wrong as the "automated" guess it replaced.
So the loader reports the failure instead of inventing an answer, and
the form says so: Create stays disabled, with a line explaining why and
a link to try again. Verified in the browser both ways — unreadable
policy disables Create and shows the message, a healthy load prefills
the name and enables it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): close the advanced-settings bypass while the policy is unknown
Create was gated on knowing whether the instance derives usernames, and
the link beside it went to a form with no such gate — so the way around
the block sat next to it. It is disabled until the policy is known, with
a title saying why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): reload the list on dismissal, and never re-offer a workspace that exists
Closing a landed import with the X left the home list stale: only
`finish()` reloaded it, so a workspace that now holds a project kept
showing its placeholder rows. A run that started wrote items whether it
finished, was abandoned or failed partway, so any dismissal after one
reloads.
Creation reported failure for a failed *list refresh* too, and handed
the form back — where a retry picks the next free id and creates a
second workspace. Once `createWorkspace` returns, nothing may report
failure: the refresh is logged if it fails, and the hand-over proceeds,
since the workspace is real either way.
The disabled-link tooltip also claimed the settings could not be read
during the ordinary load, before anything had failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): reload after the abandoned run stops, not when it is asked to
`abandon()` stops the run at the next phase boundary; the request already
sent still lands. Reloading the list at that moment could read it before
that write committed, leaving the caller stale again — the thing the
reload was added to fix. It now waits for `running` to clear, which is
immediate for the common case of dismissing a finished import, with a
cap so a run that never settles still ends in a reload.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): give the list reload one owner, taken by both exits
Finish reloaded immediately while dismissal waited for the run to stop,
so Finish pressed during a retry — `done` survives one, which is what
makes the button clickable then — read the list mid-write, and its
`finishing` flag stopped the deferred reload from correcting it.
Both exits now go through the same wait. One reload per closing, always
after the writing stops, whichever way the dialog was left.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* feat(frontend): give ImportExecution a whenIdle(), and await it instead of polling
The reload waited on a 250ms poll of `running` with a 15s cap, because
the modal receives the execution after `run()` was already called and so
holds no promise to await. The cap was its own hole: a write slower than
15s reloaded early, and nothing followed.
`run()` now keeps the in-flight promise and `whenIdle()` hands it out —
resolved when nothing is being written, immediate when no run is in
flight. The modal awaits that: no poll, no cap, no window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): back the settlement reload with a bound, and keep reporting run failures
`installProject` writes serially and takes no signal, so a request left
pending after earlier items committed leaves those invisible until the
next page load — `whenIdle()` alone never resolves for it. A bound now
reloads once in that case, *without* replacing the settlement reload:
replacing it was the flaw in the timeout this grew out of, so a hung run
reloads on the bound and again if it ever finishes.
`whenIdle()`'s rejection handler also swallowed the only report an
unexpected throw had — `#runInternal` has no catch of its own, and a
throw outside its inner ones leaves a stalled run with nothing on
screen. It logs now instead of discarding.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* feat(frontend): say when a workspace holds only archived items
A workspace whose items are all archived read as empty, because the
placeholder is decided by the default listing. Reaching those items then
depended on the toolbar, which is why it had been left interactive while
dimmed — and that let a kind toggle replace the invitation with "no
items found" on a workspace that really was empty.
The state is named instead. When the default listing comes back empty,
one request asks whether anything archived exists, and the placeholder
says which of the two it is: "Everything in this workspace is archived"
with a link to show them, or the ordinary invitation. Held until that
answer lands rather than drawn and swapped, since the wrong one claims
the workspace is empty when it is not.
The toolbar is dimmed and `inert` again, its original design: the
archived case now carries its own way in.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix(frontend): make the archived route independent of write permissions
Three ways the archived-only placeholder failed to deliver what it
promised:
Reading archived items is not a write, but the notice offering them sat
behind the create-permission gate — so an operator, or a workspace whose
direct-deploy protection cleared `showEditButtons`, got "no items found"
over items it could see and a toolbar now inert. The gate governs the
create actions alone; the notice is shown to whoever the probe found
something for.
The probe answered once per workspace and was never invalidated, so
archiving the last item left a cached "nothing archived" claiming the
workspace was empty until a page load. `reloadItemsAndCounts` clears it.
And it omitted `includeWithoutMain`, which the backend reads as
excluding library scripts — a workspace holding only archived ones
answered "empty". Always true here: hiding library scripts puts a filter
in `activeFilters`, which `workspaceEmpty` requires to be empty.
`whenIdle()` gains the two tests its contract deserves, since the reload
correctness three rounds argued over rests on it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix: harden the hub proxy, the workspace picker's gating and the rd hand-off
Findings from four local review passes over the branch:
- `list_projects` refuses when `disable_hub` is set, and `is_public_hub` now
compares the parsed host, so no spelling of the public hub (mixed-case scheme
or host, port, trailing dot, userinfo) forwards a member's bearer token there.
Covered by a unit test table.
- The workspace picker waits on `usersWorkspaceStore` as well as `workspaces`,
which derives to `[]` while the store is unloaded; with the create-form latch,
one such frame swapped a member's picker for the create form until reload.
- `refreshSuperadmin` takes `force`, and the picker uses it: a `false` left over
from a logged-out load decides whether the page is a picker or a create form.
A cancelled call no longer publishes `false` over the live request's answer,
and only its own request's handle is cleared.
- `rd` is sanitized once where it is derived rather than at each of the four
hand-offs, so an absolute target keeps the OAuth callback's allowance and
`https://evil.example/` is dropped.
- The archived-items probe answers "unknown" on failure, which keeps the
ordinary caption and leaves the toolbar reachable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* fix: answer the round-33 nits on the hub route and the empty state
- `empty_state_view` is no longer logged for an archived-only workspace, which
is not the state the counter measures.
- `detail`'s fetcher keeps its last answer in a local instead of reading
`detail.current`, a self-reference that typed the resource `any`.
- `list_projects`' comment, including its authorization contract, is back on the
handler rather than on the predicate inserted above it.
- `listHubProjects` documents the 400 an instance with the hub disabled returns.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
* feat(frontend): keep the operator onboarding tour
Operators cannot create anything, so the home page is the whole product to
them and its three tabs are worth naming. The tour that did that is the one
piece of the removed tutorial system that still has an audience.
Restored trimmed: driver.js, the driver wrapper and its controls, the
`.driver-popover` styling, and a module for the progress bit. The catalogue
machinery it used to sit in — the config, the role gating, the router, the
banner and the tutorials page — stays deleted, so the five steps are reached
directly instead of through a registry of one.
It runs on an operator's first home page visit and is recorded as seen
however it ends, including navigating away; afterwards it is in the sidebar
menu under Take the tour, which is where the last step points. Progress uses
the surviving `tutorial_progress` route, slot 6, read-modify-written so the
slots of the removed tutorials keep their state.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JirHCYVR6qg7Xqe4PcZ1KG
* fix(frontend): dim the home toolbar only where the placeholder replaces it
Standing the toolbar down depends on something else offering a way onwards.
An operator in a workspace that is simply empty gets no placeholder — they
cannot create, and there is nothing archived to reach — so the search and the
kind toggles were the only controls on the page, dimmed to 40% and `inert`.
They now follow the placeholder rather than emptiness, which also stops the
operator tour spending three of its five steps highlighting controls this
page had greyed out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JirHCYVR6qg7Xqe4PcZ1KG
* docs(frontend): put the inline-link rationale on the first link, not the third
The caption's three links share one reason for being bare `<button>`s, and it
was written on the last of them. A reader — or a reviewer — meets the archived
one first.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JirHCYVR6qg7Xqe4PcZ1KG
* fix(frontend): keep private hub project names out of telemetry
An instance pointed at its own hub imports its own projects, and the slug
naming one is the customer's content — `template_import` was recording it
verbatim, which the disclosure ("the name of any public hub project") does
not cover and `hub_script` already avoids by collapsing a private script to
`private`.
`hubProjectUsageKey` gives projects the same treatment, deciding by the
configured hub's host so a port, a scheme's case or a trailing slash cannot
turn a private hub into a public one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JirHCYVR6qg7Xqe4PcZ1KG
* fix(frontend): treat an unread hub setting as private, not as the public hub
`hubBaseUrlStore` is seeded with the public hub and written in one place, by a
loader with no catch and no retry. A settings read that threw therefore left
the store naming hub.windmill.dev for the rest of the session, and the import
counter read that as permission to report a private instance's project slug —
the leak the previous commit closed, narrowed to "after one failed read".
The fact has three states and the store held two, so `hubBaseUrlKnown` carries
the third: the loader sets it only once the value is the instance's own, and
the telemetry key requires it. Links keep rendering the default meanwhile,
which is what they always did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JirHCYVR6qg7Xqe4PcZ1KG
* fix(frontend): tag each hub detail with the slug that asked for it
`resource()` assigns whatever its fetcher returns, with no guard for a run that
has been superseded, so handing back the previously fetched project on a stale
response published that project. With two slow requests in flight — pick A,
leave B loading, pick C — B's answer put A's name, author and counts on the
card while the plan underneath still said C, and Import wrote C.
Each answer now carries its own slug and is read only while that slug is the
chosen one, which also drops the local the previous shape needed to keep the
resource's type from going circular.
The hub-telemetry tests reset their shared fixture per case; the private-hub
one had been passing on what the case above it left behind.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JirHCYVR6qg7Xqe4PcZ1KG
* fix(frontend): stop the template picker spinning on a hub with no projects
Opening the picker against a reachable hub that has published nothing pinned
the renderer at full CPU and froze the tab. The effect arming the list called
`setLoader` and `loadData`, which read `InfiniteList`'s reactive state as well
as writing it, so the effect depended on what its own load changed and re-ran
itself; a list that stays empty never settles that cycle. It now arms the
loader once per workspace, untracked, and leaves the load to `setLoader`.
The same empty list also claimed the hub was unreachable, since one `empty`
snippet serves both. The loader records which happened, so a hub with nothing
on it says so.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JirHCYVR6qg7Xqe4PcZ1KG
* fix(frontend): keep a superseded hub answer from erasing the chosen project
`resource()` publishes whatever its fetcher returns, superseded or not, and
`fetchHubProject` takes no abort signal — so an answer for a project the user
had moved on from replaced the published value, the slug guard rejected it,
and the chosen project's item counts went off the card for good with nothing
left to ask for them again.
The fetch now records its own answer, tagged with its slug and only while that
slug is still the chosen one, and the card reads that. Nothing reads the
resource, so it is a `watch` — the same machinery without the value that was
the problem.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JirHCYVR6qg7Xqe4PcZ1KG
* chore: update ee-repo-ref to 81edd1382d951265ab3e9b67fc7ca7967676fd56
This commit updates the EE repository reference after PR #775 was merged in windmill-ee-private.
Previous ee-repo-ref: 21ace847ec1c1406bafc50153004e1874642bf6c
New ee-repo-ref: 81edd1382d951265ab3e9b67fc7ca7967676fd56
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat: let a native trigger be disabled without deleting it
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: show and control the native trigger pause outside the flow editor
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: create a native trigger already paused instead of pausing it after
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: correct the native trigger enabled comments for create-time init
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(nativets): bound fetch on a peer that never answers
deno_fetch applies no deadline of any kind. A peer that completes the TCP
handshake, accepts the request and then goes silent leaves `await fetch(...)`
pending indefinitely, holding its worker slot until the *job* timeout -- which
on self-hosted defaults to DEFAULT_SELFHOSTED_TIMEOUT, i.e. 7 days.
Nothing else catches this. Zombie-job detection keys off a stale
v2_job_runtime.ping, and a worker blocked inside a pending fetch keeps pinging
normally throughout: the worker is alive and healthy, only the work is dead.
What this bounds is the wait for a response to begin, and it stops there:
- a peer that never answers -> rejected after N seconds
- a peer slow to answer, but under N -> unaffected
- a body that then streams for an hour,
or is read slowly by the caller -> unaffected, always
That last line rules out the obvious implementation: AbortSignal.timeout(N)
around every fetch would bound the hang and break every streaming response and
long download. This is a hang detector, not a latency budget.
Default 300s via WINDMILL_FETCH_RESPONSE_TIMEOUT_SECS (0 disables), with a
per-script `//fetch_response_timeout <seconds>` annotation alongside the
existing //useragent and //proxy. Both nativets paths inherit it, since
eval_fetch_timeout and the dedicated-worker path in bun_executor both funnel
through create_nativets_runtime.
The ms value is clamped to i32::MAX: deno_web's setTimeout runs its delay
through webidl.converters.long, a 32-bit conversion that *wraps*, so a setting
past ~24.8 days would come out negative and fire immediately -- turning an
over-generous timeout into an instant one on every fetch.
The window covers connect, TLS and request upload as well as server think
time, so a very slow large upload is bounded by it too; the error message says
so rather than claiming the connection went silent.
Not covered: a body that stalls midway. Reaching that needs the response's
InnerBody, which deno_fetch keeps module-private, and every way to wrap it
from outside changes observable Response semantics (locking, bodyUsed,
double-consume errors). Left for a follow-up in deno_fetch itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JshNT5XVH78ZsTMvDWfFHb
* test(nativets): cover the instance-wide response-timeout env var
A typo in WINDMILL_FETCH_RESPONSE_TIMEOUT_SECS would compile, pass every
other test, and silently hand every operator the 300s default -- the same
class of silent-default failure the timeout itself exists to prevent. Its
own test binary, since a LazyLock resolves the value once per process.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JshNT5XVH78ZsTMvDWfFHb
* fix(nativets): inherit the caller's RequestInit, and clear the long-poll ceiling
Two problems with the first cut, both found in review.
`{ ...init, signal }` copied only own enumerable properties, but RequestInit is
a WebIDL dictionary whose members deno_fetch reads with plain property gets
that walk the prototype chain. Anything inherited or non-enumerable was
dropped: `fetch(url, Object.create({method: "POST"}))` silently became a GET.
Worse, a non-object init went from a loud TypeError to a silent GET, because
spreading "POST" yields {0:"P",1:"O",...} -- a valid dictionary with ignored
keys. Now the init is inherited from rather than copied, and a non-dictionary
is handed straight back to deno_fetch for its own TypeError.
The 300s default also sat at half of TIMEOUT_WAIT_RESULT (600s), which
run_wait_result long-polls against with no response headers. A script running
another job synchronously for 300-600s would have timed out client-side while
the server was still legitimately holding the request open -- the long-poll
risk class, instantiated inside the product and reachable without writing a
raw fetch. Default raised to 900s, with the constraint recorded where someone
would break it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JshNT5XVH78ZsTMvDWfFHb
* fix(nativets): hand the caller's RequestInit to Request untouched
Carrying a WebIDL dictionary across by hand has no safe form, and both
previous attempts were wrong in opposite directions. Spreading a copy drops
inherited and non-enumerable members, and turns a non-object init from a loud
TypeError into a silent GET. Inheriting from it via Object.create fixes those
but makes the child the receiver, so an accessor on the original runs against
an object that lacks its private-field brand:
Cannot read private member #body from an object whose class did not
declare it
So don't carry it at all. fetch()'s own first act is `new Request(input,
init)`; doing that here hands the init to the same constructor, read exactly
as it would be without this wrapper, and our signal travels in an init we own.
`req.signal` is then deno's own resolution of init.signal over an input
Request's signal, which removes the hand-rolled version of that rule too.
The Request is built twice as a result, once here and once inside fetch. That
is cheap: cloneInnerRequest carries method, headers, redirect mode, clientRid
and blob entry, and a body is proxied rather than buffered -- a static body is
a shallow {body, consumed} copy sharing its bytes, a stream gets a one-chunk
pass-through.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JshNT5XVH78ZsTMvDWfFHb
* fix(nativets): keep an aborted fetch settling in the same tick
deno_fetch keeps its outer fetch non-async on purpose: "WPT has a test that
aborted fetch is settled in the same tick. This means we cannot wrap the
promise if it is already settled" (26_fetch.js). An `async` wrapper adopts
that promise through another one, so a rejection that used to land before any
microtask queued after the call now lands after it.
Made the wrapper non-async, with an early return that hands deno's settled
rejection straight back for an already-aborted signal, and no timer armed
there since there is no response to wait for. Construction still has to reject
rather than throw, so it is caught and returned as a rejection, which is what
the `async` was buying.
The comment claiming this matched deno_fetch's own `async function fetch` was
wrong on two counts -- that function is not async, and the wrapper was not
matching it. Replaced with the constraint that actually holds.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JshNT5XVH78ZsTMvDWfFHb
* fix(nativets): keep fetch's observable shape and reach intrinsics safely
Three ways the wrapper was distinguishable from the fetch it replaces, all
observable from a script sharing the isolate.
`.then` was an ordinary property lookup, so `Promise.prototype.then =
undefined` broke fetch after the request had already gone out. deno's own
modules reach intrinsics through primordials, and this file already captured
setTimeout, clearTimeout and Promise.reject for exactly that reason, so the
lookup was the odd one out. Now captured alongside them.
Declaring `init` without a default made `fetch.length` 2 where the standard
says 1. And the empty-call branch forwarded two explicit `undefined`s, so
deno's required-argument check saw two arguments and raised "Invalid URL:
'undefined'" instead of "1 argument required". Forwarding through
ReflectApply preserves the count.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JshNT5XVH78ZsTMvDWfFHb
* docs: clarify fetch timeout restart requirements
* fix: capture native fetch abort helpers
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: recognize `// volume:` mounts in PHP scripts
Volume annotations were parsed for every language but PHP, so a PHP script
could not mount a workspace volume. Two things stood in the way: PHP had no
entry in the comment-prefix maps, and a PHP script opens with `<?php`, which
ends the leading comment block the parsers scan before any annotation is read.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3FR7iS9nRhpFt615cnuQ7
* fix: tolerate a PHP opener that carries code, drop the inert CLI hunk
The open-tag skip matched `<?php` exactly, so `<?php declare(strict_types=1);`
still ended the leading comment block and every annotation below it was silently
ignored. Match the tag as a case-insensitive prefix and skip the whole line.
The CLI local-graph hunk could never fire: PHP has no wasm asset parser, so
`fallbackParse` handles it, and its own header scan stops at `<?php` — the script
is dropped as a non-pipeline-member before any volume asset is read. Making only
the CLI PHP-aware would also put the local graph out of parity with the deployed
one, whose `parse_pipeline_annotations` stops there too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3FR7iS9nRhpFt615cnuQ7
* docs: correct the CLI mirror comment, state the own-line annotation rule
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3FR7iS9nRhpFt615cnuQ7
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: refresh a dbt column trace with its graph, and stop calling whole ones cut
Follow-up to #11014, addressing two findings from the review round that landed
after it merged.
**A deliberate graph refresh now re-asks for the lineage.** The dedup key held
only the workspace, the pin and the seed relations, all of which a redeploy
leaves alone — so the pipeline page's Refresh refetched the graph and left the
trace as it was, pairing the new version's SQL and columns with the previous
one's edges. The key now carries which fetch of the graph is on screen, taken
from `graphRes.current`'s identity: it moves on a Refresh, a deploy and a folder
switch, and on nothing else, so an editor keystroke still cannot make the pane
re-ask.
**`truncated` is set only with evidence.** `pending` was read as proof the
component had been cut, but it only says a relation's owners have not been asked
about yet — and those owners are usually the project already in hand. A project
holding more than the expansion budget across unrelated families therefore
reported a small, complete component as truncated. The owners query now runs
before the budget and round stops, so a trace is called cut only when a project
this caller may read is left unread, or when the walk itself was cut.
Two smaller things from the same round: a failed lineage request says so instead
of rendering the empty trace a project without the analysis pass renders — the
two were indistinguishable, and a Refresh now retries it — and `asset_paths` is
capped as well as refused when empty. `MAX_HELD_EDGES` is renamed
`EXPANSION_EDGE_BUDGET`: it never bounded what its name claimed, since the seeds'
own projects are read whole whatever their size.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NY4kuFy2jAnGEzaCc1CseL
* fix: show a failed column-lineage request beside a partial trace, not only instead of one
Review-round findings on this PR.
The failure line only rendered when the trace had no nodes at all. A ducklake
selection whose producers declare column lineage has nodes from the graph the
canvas already carries, so a failed dbt request left a trace that rendered and
was missing a half — which is the reading the line exists to prevent. It now
renders beside a drawn trace as well, and says the trace may be incomplete
rather than that nothing loaded.
The dbt branch of the details pane also opened on `selectionColumnLoading` but
not on the failed state, so a relation with neither SQL nor a column schema fell
through to "no inline preview" and the line never rendered at all.
Dropped "Refresh to try again": the dbt editor has no Refresh for this, and its
recovery is a re-parse or reselecting. The comment on the error handler says both
paths again rather than only the one the pipeline page uses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NY4kuFy2jAnGEzaCc1CseL
* fix: clear a column-lineage failure when the next request goes out
Round-2 nits, both reviewers on the same state.
`failed` was cleared only when an answer landed, so a retry kept saying the trace
may be incomplete while it was being fetched, and a new selection inherited the
previous one's failure until its own answer arrived. It is cleared as the request
goes out instead.
Also documents the bounds on `asset_path` in the two routes that take it: the
1000-relation cap and the at-least-one rule were both enforced and neither was
written down, so a caller met them as a 400 with no way to have known.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NY4kuFy2jAnGEzaCc1CseL
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Serves the edges `dbt_column_edge` has been storing. `assets/column_lineage`
answers the connected component a set of relations' columns sit in, and the
details pane draws it beside the model's SQL — in the dbt editor, on the
pipeline page, and for a run through `jobs/dbt_column_lineage/{id}`.
The unpinned component crosses projects. A relation one project produces is
another's source, so resolving owners once — for the relations asked about —
stops the trace at the first boundary. Owners are resolved to a fixpoint
instead, and the caller's gate is re-applied to every project the expansion
discovers: reaching a relation says nothing about who may read the project on
the far side of it. A pinned answer needs none of it, by version or by job: the
pin says which stored graph is on screen, and another project's live graph is
not part of it.
One request per selection, whatever it reaches: the endpoint takes every
relation at once and answers their union, so nothing is held between selections
and there is no staleness, retry bookkeeping or per-click dedup to balance.
The answer is bounded. A synthetic 3000-model project whose models share a
column has 58k direct edges and returns 7.3MB, which no column diagram can draw;
the walk is breadth-first from the asked-for relations and stops at 5000 edges,
so what survives is the part nearest the selection, and `truncated` says the
trace was cut rather than ended.
Also adds the columns section the pipeline page's asset pane was missing, so
`column_schema` is visible there and not only in the dbt editor.
Claude-Session: https://claude.ai/code/session_01NY4kuFy2jAnGEzaCc1CseL
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: column-level lineage for dbt from the engine's parquet index
`manifest.json` carries no column-to-column edges, which is why decision 14
recorded column lineage as unavailable. The edges live in a different artifact:
`dbt compile --static-analysis strict --write-index` writes `target/index/`,
whose `dbt.column_lineage.parquet` holds them and whose
`dbt.node_columns.parquet` holds every column of every node, typed and ordered
rather than only the ones an author documented.
Strict analysis rejects SQL the default accepts, so this is a separate compile
with its own `--target-path`, opt-in per project via `column_lineage: true`, and
best-effort throughout: a project it cannot analyze keeps exactly the graph it
had, with the engine's own diagnostics in the job log.
Storage mirrors `dbt_edge`: `dbt_column_edge` keyed by (path, version, job) with
the same composite FK to `script` and the same sweeps. The typed column list
lands in `dbt_node.column_schema`, beside `columns` rather than merged into it,
so `columns` stays what the author declared.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PRtsPQ3Ck69Fu9DNr7bMcJ
* fix: address review findings on the dbt column-lineage pass
- The workspace fork copied every other dbt sidecar table and not this one, so
a fork lost its column lineage silently and could not recover it: the cloned
digest covers the column edges, so a dynamic run in the fork matched it and
stored nothing.
- The parquet was collected whole before the edge cap applied, which is exactly
the input the cap exists for — a project whose `scan` lineage is quadratic in
its widest model could take the worker process down. Decoded a row at a time
with the bound enforced during the decode.
- The pass swallowed every error from the runner, including the job poller's
cancellation and deadline, so a run that blew its timeout inside an optional
annotation could still publish a graph and report success. `run_captured`
now carries the exit status in its value, so only a failed COMPILE is
downgraded, and the pass may spend at most half the remaining wall clock so
it cannot starve the build that follows it.
- `scan` edges are stored but no longer served: they are most of a project's
lineage, nothing renders them, and the graph endpoint is polled by the run
page. They are also the first thing the storage cap gives up now, rather than
evicting the direct edges the trace draws.
- `column_schema` and the column edges take the same gate as the model's SQL. A
column-level view is the shape of what the author wrote, one level finer than
the `ref()` graph, which is ungated only because it draws relations the
caller already sees.
- `graph_digest` hashes the new section only when it has edges, so a project
that never asked for the pass keeps the digest it has instead of
re-snapshotting on every dynamic run until it is redeployed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PRtsPQ3Ck69Fu9DNr7bMcJ
* fix: the editor buffer's column lineage, and three bounds that were wrong
Round-2 review found four defects, all of them introduced by the round-1 fixes.
- The `script_visible` gate on the column edges was copied from the node query
without its `script_hash IS NULL` arm. `= NULL` is never true, so every
version-less row was filtered out and an editor buffer's parse rendered its
typed columns and none of their lineage — the one place the feature is meant
to be used. Pinned by an assertion in `dbt_pinned_graph.rs`, which is where
this class of bug already had a home.
- The phase budget was handed to the poller, whose expiry is an `Err`
indistinguishable from a cancellation or the job's own deadline, so a slow
but valid analysis aborted the build it exists to annotate. The runner gets
the full deadline again — those two must still fail the job — and the budget
is a race around the whole pass, where expiring is this budget and nothing
else.
- The decode cap counted parquet ROWS, so `scan` and out-of-graph rows could
spend it before a single drawn edge was read. It now counts what is kept,
takes direct kinds in a first pass, and is handed the graph's own nodes so
the budget cannot go on rows that could never be stored.
- Hashing the new digest section conditionally did not preserve old digests,
because an absent `column_schema` still serialized as `null` inside the
nodes. It is skipped when absent instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PRtsPQ3Ck69Fu9DNr7bMcJ
* refactor: split the lineage pass by error contract, and read it in one query
Round 3's findings were all consequences of round 1 and 2's fixes, clustered in
the same two files, so this reshapes those two seams rather than patching again.
The worker pass was one function being three things at once — a subprocess
runner with job-lifecycle error semantics, a bounded decoder, and a best-effort
degrader — which is why each fix to one perturbed another. It is now
`compile_index`, which owns the JOB's semantics (only a cancellation or the
job's deadline can `Err`; a non-zero exit, the output ceiling and the phase
budget are outcomes), and `read_index`, which owns the ARTIFACT's and knows
nothing about the job. The budget wraps the compile alone, so a decode can no
longer outlive the timeout that reported the build would get the rest. The
output ceiling likewise becomes a value rather than a job error, for the caller
that can carry on without the tail of a compile's stdout.
The column edges were read by a fourth hand-written copy of the `live`/`chosen`
CTEs and the version/editor-buffer join conditions, and copying them is what
dropped the `script_hash IS NULL` arm and hid every buffer parse's lineage. Both
kinds of edge now come from ONE statement over a `UNION ALL`'d edge source, so
those conditions exist once. The union is at the source rather than a join
because column lineage can name a node pair `dbt_edge` has no row for: a model
reading `{{ this }}` gets edges from itself to itself, and `parent_map` has no
self-loop.
The cap on the column half now sits after the scope filter, the visibility
check and the graph joins — the scope moved into SQL via the existing
`ScopePathFilter` — so a row the caller may not read can no longer spend it and
leave an allowed project's trace short.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PRtsPQ3Ck69Fu9DNr7bMcJ
* refactor: serve dbt column lineage from its own endpoint
The column edges rode on the folder-wide asset graph, which a run page polls,
while the trace is drawn for one selected relation. That needed a cap, and a cap
has to be applied after every filter that can drop a row.
Keyed to the asset there is no cap: `assets/column_lineage` answers for one
relation, and the caller's `scripts:read` scope and the project's visibility are
decided once, for the script that owns it. Pinning to a run's snapshot or the
editor's parse of its buffer costs the job-read gate, so that form is
`jobs/dbt_column_lineage/{id}` — the same shape `jobs/dbt_graph/{id}` has.
The worker's decode now bounds work and memory separately, and a compile stopped
by the output ceiling reports as truncated rather than complete.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: resolve the owning dbt version the way the graph does
The unpinned arm picked the newest live version at the path without narrowing to
dbt, so a path since redeployed in another language answered with no lineage
while the graph beside it still drew that project's stale nodes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: gate pinned column lineage on reading the project, and answer the component
Four things round 5 found, three of them in code this branch rewrote:
- The pinned arm resolved the version from the job and stopped there, so a
share-link viewer entitled to a run got the project's column names and edges
while the graph beside it still redacted `raw_code` and `column_schema`.
Resolving WHICH version answers is not deciding whether the caller may read
it; the version-less editor buffer keeps its exemption, having no `script` row
to ask.
- The answer was the whole owning project's edges. The canvas lays out the
connected component of the selected relation's columns, so the rest was
unrenderable weight; a recursive walk over both directions returns exactly
what is drawn, and the project key travels with it so a `unique_id` two
projects share cannot walk from one graph into the other.
- The decode had no exit but the 4M-row backstop once its buckets were full,
spending wall clock the build below does not get.
- An unreadable index was reported as a missing one, sending the reader to look
at their engine rather than at the file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: stitch the two column graphs, and walk the component in Rust
Round 6's two findings, both regressions this branch introduced:
- The decode returned `Continue` on the edge that FILLED the direct-edge
budget, so a `scan`-only tail after it decoded to the 4M-row backstop with
nowhere to put anything. The read now ends on that edge.
- Seam 3 made the pipeline page choose between the dbt graph and the producer
one. They share node ids — `// column total <- dbt://wh/analytics/orders.amount`
mints the same `(dbt, path, column)` node dbt's own lineage does — so choosing
ended a trace at the boundary in both directions. They are merged again, and
a ducklake selection asks about the dbt relation its producers name so the
chain continues past it. The dbt editor gets the same merge.
Also: the component is walked in Rust rather than by a recursive CTE. A CTE has
no index, so the recursive term rescanned the doubled edge set once per level —
1243ms against 59ms for the query alone on a 3000-model project, 11.7M rows in
the plan. Same answers, same tests; end to end 1.48s to 0.73s there and 1.60s to
0.26s on a 1000-deep chain. The client stops re-asking for a component it
already holds, which is most clicks within one project.
The four doc sites that described a whole-project answer are rewritten around
what it now is, rather than edited where they disagreed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: expand every dbt boundary a selection reaches, and only skip what was asked
Round 7's findings, all in the frontend seam this branch added:
- A ducklake selection seeded the dbt fetch from the FIRST boundary relation it
found, so a table derived from two unconnected dbt relations expanded one and
left the other a leaf — the same "stops at the boundary" symptom the round-6
fix removed, one hop further along. Every distinct boundary is fetched now and
the components merged.
- The component cache skipped a relation merely PRESENT in the graph in hand.
A relation two projects describe has an owner row in each, and a component
fetched for one carries it as an endpoint without the other's half, so that
skipped the request that would have resolved the second owner. Only a relation
actually asked about under this pin is skipped.
- A comment still called the producer graph gated to ducklake selections after
it was widened to dbt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor: land dbt column lineage as storage and ingest only
The API surface that draws a column trace moves to a follow-up PR, on
`dbt-column-lineage-surface`. It kept generating findings — a client cache
whose premise was wrong for a two-owner relation, then staleness and a lost
retry from tightening it, and a seed walk that stopped at the first boundary —
and the fix for the last of them is a transitive owner expansion, which has to
re-apply the caller's gate to every newly discovered project. That is the same
shape as the leak four reviewers caught in the pinned arm, and it wants its own
review rather than being the fourth fix at the end of this one.
What lands here stands on its own: the analysis pass, `dbt_column_edge`,
`dbt_node.column_schema`, the engine gating and the error-contract split — plus
the one user-visible half, the typed and ordered column list, which rides the
asset graph the details pane already fetches and replaces a panel that could
only show the columns an author had documented.
Also fixes a real bug in the pass, found in review: it compiled without the
build's `--full-refresh`. `is_incremental()` branches on that flag, so an
incremental model reading `{{ this }}` compiles its self-join — and any `ref()`
inside that branch — only when the flag is absent, and the pass was storing
lineage for SQL a full-refresh run never executed. The flag now comes from one
place shared with the build, and a run that overrides it gets its own graph
rather than standing as the version's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: say why direct kinds get the budget without naming a view
The bucketing comments explained the priority by what a trace draws, which is
a forward reference now that the surface moved out. The reason stands on its
own: `copy`/`mod` say the value travelled, `scan` says the column was read to
produce the row and so reaches every output column of its model.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: round-9 findings on the descoped PR
- The `full_refresh` helper was inserted between `selection_is_overridden` and
its doc comment, so thirteen lines about `select`/`exclude` echoes documented
the wrong function and the one they were written for had none. Moved below it.
- The parse path ran the analysis compile and the parquet decode BEFORE the
guard that returns when there is no warehouse identity, paying for both and
dropping the result. Moved after it.
- Three sites still described a `/column_lineage` endpoint this branch no longer
has, and two user-facing strings promised a column trace it no longer renders:
the panel's hint and the descriptor template now say what the flag actually
buys, which is the typed column schema.
- Dropped test scaffolding the removed suite left behind: a `raw_orders` node
and `dbt_edge` whose only assertion re-tested pre-existing graph behaviour,
and a second editor-buffer node nothing asserts on.
Documented rather than fixed: an incremental model has two shapes, and which one
the index holds depends on whether the target existed when the pass ran.
`is_incremental()` is false with no target as well as under `--full-refresh`, and
dbt has no mode that emits both — so a version's graph describes the compile that
produced it, and only a re-ingesting run describes its own run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: keep lineage_kind in the edge key, and one answer for --full-refresh
- Both unique indexes omitted `lineage_kind`, so a column that is projected AND
used as a predicate for the same output column — an ordinary shape — had its
`copy` and `scan` edges collapse under `ON CONFLICT DO NOTHING`, while the
digest counted both. The kind is part of the fact, so it is part of the key.
Edited in the migration rather than added as a second one: it has not landed.
- `full_refresh` was shared between the build and the analysis pass without the
`command != "test"` condition that sat at the build's call site, so the two
disagreed for exactly the runs that build nothing. The condition moved inside
the function, which is the point of sharing it, and the command is threaded to
the pass.
- The "what a trace draws" rewrite missed the copy in `dbt_manifest.rs`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: drop the unreachable full_refresh threading, test the uniqueness key
`DBT_COMMANDS` is `["build", "retry", "show", "parse"]` and `default_command`
returns `build` in every arm, so `command == "test"` cannot happen — the guard
the last commit moved into `full_refresh` was already inert where it came from.
Threading the command through five signatures to preserve it bought nothing, so
it is gone; the build and the pass call one function of the descriptor and the
invocation, which is what the sharing was for.
The uniqueness-key fix now has a test: a column projected AND used as a
predicate for the same output column stores both its `copy` and its `scan` row.
Verified against the old key, where it returns 1 instead of 2.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: restore the dbt test --full-refresh guard I removed on a wrong premise
The previous commit removed it after reading `DBT_COMMANDS` and concluding
`"test"` was unreachable. That is only true of the command a CALLER can name:
`run_dbt` is invoked with `"test"` directly for the `after_all` test phase, so
an `after_all` project with `full_refresh: true` reached it — and dbt rejects
`--full-refresh` on `test`, failing the phase. Both reviewers caught it.
The guard is back inside the shared function, where the build and the pass get
one answer, and its doc now records why reading the allowlist alone is
misleading. The test covering the `test` case is restored with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: notice a job that ended during the decode, and name truncation as the cause
- The parquet decode runs on a blocking thread with no poller watching it, so a
cancellation or an expired deadline during it was invisible: `dbt_dep` went on
to publish the graph and the job returned success. The job's state is checked
once the decode returns, before the caller publishes anything, and an ended
job `Err`s — which this module may always do for the job's own semantics.
- A compile stopped by the output ceiling could leave no artifact, and the log
then blamed the engine's capability, sending the reader to check their adapter
rather than the ceiling. Truncation now names itself in the missing and
unreadable branches too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: read cancellation from the DB after the decode, not from a poller's field
`ctx.canceled_by` is only ever written by a poller, and no poller runs during
the blocking decode — which is the exact window the check was added for. So the
guard caught only a cancellation already observed before it, and the comment
beside it claimed more than it did. It now queries `v2_job_queue` directly, the
same probe `worker_lockfiles` uses before it overwrites a flow.
A failed probe answers "still running": this decides whether to discard work
already done, so an unreachable database must not be the reason a healthy deploy
loses its graph.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor: reuse job_is_canceled rather than a second copy of it
The probe added last round was `job_is_canceled` from the same file, retyped —
same query, same `Connection::Http` behaviour. Reused instead.
Its doc said a non-database connection was "a failed probe", which reads as an
error path. It is not: it is the agent worker, and on one there is no database
to ask, so only the deadline answers and a cancel issued during the decode is
not observable. The retry path avoids that by refusing to run on an agent worker
at all — which an optional annotation has no business doing — so the gap is
recorded at both ends instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: close the agent-worker cancellation gap instead of documenting it
The previous commit said a cancel issued during the decode is not observable on
an agent worker. It is: `ping_job_status` returns `canceled_by` over both
connection kinds, and is how the poller itself notices one there. So the check
asks through the ping rather than querying `v2_job_queue` directly, and holds on
an agent worker, where a direct query reaches no database at all.
`job_is_canceled` goes back to private and its doc to what it said before — the
retry that calls it still refuses to run on an agent worker for its own reasons.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: decode the index under the job poller instead of checking after it
Two findings with one cause: the decode was the only phase of this pass with no
subprocess behind it, so nothing heartbeated while it ran. A large index left
the worker silent for as long as it took, which the zombie sweep reads as a dead
job and restarts — and the cancellation check bolted on afterwards could only
ever report what had already happened, while dropping the ping's
`already_completed`, so a force-cancelled deploy still published its graph.
Running it under `run_future_with_polling_update_job_poller` answers all of it:
the poller pings throughout, and ends the phase with an `Err` on cancellation,
`AlreadyCompleted` or the phase timeout. The bespoke probe is gone with it.
Verified on a live deploy: 32 edges and 4 typed schemas ingested through the
polled decode.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: stop a cancelled decode, and say what the read phase can now do
Putting the decode under the poller heartbeats it and ends the phase when the
job does, but dropping a `JoinHandle` detaches a blocking task rather than
cancelling it — so a cancelled job left a thread decoding up to four million
rows for a job that was over. The row loop reads an abandonment flag that a drop
guard on the awaiting future sets, so the decode stops at its next row.
That same change made the read phase able to `Err`, and three places still said
it could not — decision 14 in as many words. The distinction that holds is
narrower: nothing the ARTIFACT does or fails to do can fail a job, so absent,
unreadable and partial are all values; the JOB can still end the phase the read
runs in. Stated that way in the module doc, the `Artifact` doc, `MAX_INDEX_ROWS`
and the decision.
Verified on a live deploy: 32 edges and 4 typed schemas.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor: share AbortOnDrop, and stop citing a hazard that is now handled
`Abandon` was `ansible_executor`'s `AbortOnDrop` retyped — same struct, same
reason, same `spawn_blocking` shape. Moved to `common` and used from both.
The paragraph explaining why the phase budget wraps the compile alone gave as
its reason "a decode still running on a blocking thread", which is exactly what
the abandonment flag now prevents. The reason that survives is the one that was
always the point: the budget exists to leave the build its share of the clock,
and only the compile can spend that share unboundedly. The decode's end is the
job's, through the poller it runs under.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: put both doc comments back on the items they describe
Moving AbortOnDrop orphaned a doc at each end: it landed between
`raw_to_string`'s doc and `raw_to_string`, and the doc of the struct it replaced
stayed behind to prefix `fetch_repo_archive`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: name the binding the row loop actually reads
`Abandoned` was neither the type nor the binding; the flag is `abandoned`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: durable dbt state per environment, and `--defer` onto it
`dbt retry` worked off two artifacts and only one was durable: `dbt_run_state`
holds `run_results.json` keyed by principal, and the manifest lived on
worker-local disk under a four-generation cache. That is enough to resume the
last run and nothing else — the next run of a project usually lands on a worker
holding neither artifact — so deferral had nothing to read.
Adds `dbt_environment_state`: one row per (workspace, script path, environment),
holding `manifest.json` and `run_results.json` from the last successful run, with
the blob inline under `DBT_STATE_INLINE_MAX_BYTES` and in the workspace's object
storage above it. Environment is the warehouse, the target, and the database and
schema they resolve to, so a repointed warehouse or a moved schema reads as an
environment nothing has published rather than as state whose relation names no
longer fit.
A run publishes it when its graph becomes what the script owns and it succeeded
— the same condition, and the same reason: an invocation that scoped its own
model set describes where the caller put those relations, not where the
project's models live.
`defer` is a `build` command-block field defaulting to the descriptor's own, and
the state is materialised into the job directory for `--defer --state`. The
retry path already did that materialisation for `dbt retry`; both go through one
`write_state_dir` now.
`--state` is also where `dbt retry` reads the run it resumes, so a retry on
dbt-core 1.x takes `--defer-state` instead, and one on an engine without that
flag is refused before the build rather than rebuilding its nodes with every
unbuilt `ref()` resolving into the schema this run writes into.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ned2pmRJwB3GpenEcrA9TF
* fix: address the local review of the dbt environment state
The oversized-artifact home moves from the workspace's object storage to the
instance's, where every other internal worker artifact already lives. The
workspace bucket is the one members read and write through `job_helpers/*` with
a caller-supplied key and only `volumes/` is reserved there, so a manifest under
it is one any member could replace — and the next deferring run would hand dbt
an attacker-chosen `defer_relation` for every unbuilt `ref()` while holding the
script's warehouse credentials.
The environment key takes the target dbt actually runs rather than the
descriptor's `profile.target`, which is absent whenever the target is inherited
from the workspace warehouse or the project's own `profiles.yml` — filing every
inherited target under one empty name, while a `target.name` macro decides where
a model is built. `write_profiles` returns a named struct now that it resolves
one more thing.
Publishing takes the row's lock before uploading, so two publishers of one
environment cannot interleave their uploads and leave one run's manifest beside
another's results, and carries the live-dbt-script guard the retry state already
had, so a job finishing after its script was renamed, archived or deleted cannot
recreate state at a path for whatever is created there next.
A rename now clears the environment state instead of moving it: an oversized
artifact's key is derived from the path, so a moved row would keep pointing at a
key a script created at the old path publishes over.
A build recovered by the automatic in-job node retry publishes its manifest
without results — `run_results.json` is then the retry's, naming only the nodes
it redid — and the refusal for an environment with nothing published names the
runs that cannot publish rather than suggesting a run that would not help.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: serialize dbt state publishers on an advisory lock
The row lock only serializes publishers once a row exists, and the first
publish of an environment — two runs of a newly deployed script — is exactly
when two of them are most likely to race and interleave their uploads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: make dbt state publication atomic and bind it to the version that ran
Every publication now writes its own object keys and the row switches to them in
one statement, so an upload never overwrites an artifact the committed row still
names: a run failing between its two uploads, or between them and its row, leaves
the state pointing at the pair it already had. The objects a commit displaces are
dropped afterwards — never before, since a reader that has already read the row
is about to fetch them — and a reader that loses that race re-reads the row once
rather than reporting a state that is there. What a publication uploaded and then
could not commit is dropped on the way out.
The write's guard names the VERSION rather than the path: the live dbt script
there must be the one this job ran, or a later version of it. "Some live dbt
script is here" is also satisfied by a script created at a path this one was
renamed away from, and this job's manifest would then become that project's
deferral state. A preview names no version and so publishes nothing.
A `show` defers too. It compiles the model it previews, so a model whose upstream
this environment built and this run did not is exactly the case a deferral exists
for, and every engine takes the flags on it.
Three comments said "the workspace's object storage" where the code deliberately
uses the instance's, which is the whole security argument; `mib()` labelled MiB
values MB.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: hold the script row across a dbt state publication, and let a rename move it
The version guard read `script` without a lock, so lifecycle cleanup could find
no environment row to clear, finish, and leave this transaction to commit state
at a path a new script goes on to occupy. It now holds that row (`FOR SHARE`) for
the rest of the publication — taken before the sidecar, the order every other dbt
writer takes — and the artifacts are uploaded before the transaction, so the lock
covers the row work rather than a network round trip.
A commit that reports an error may still have committed: what was lost can be the
acknowledgement. Dropping this run's objects then leaves the committed row naming
objects that are gone, so an orphan is the cheaper side to take.
A failed second upload left the manifest it had already written behind; it is
dropped now.
Per-publication keys retired the reason a rename cleared the environment state
rather than moving it: the path is only a prefix, and the row is what names an
artifact, so a script created at the old path can no longer publish over a moved
row. The rename moves both halves again.
`dbt ls` gets the deferral flags too, without which a `result:` selector — which
reads `run_results.json` out of the state directory, and which `select` passes to
dbt verbatim — fails before the build that would have honoured it.
Also: the migration was the last site describing the workspace's object storage
rather than the instance's, `publication_lock` folded 32 bits where it claimed
64, and `ResolvedProfile` had taken `write_profiles`'s doc block.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: a deferring dbt run never publishes the state it read
`publishes_ownership` reads the CALLER's overrides, so a descriptor that already
narrows `select` needs none and a run of it with `defer: true` published. A
deferring run built some of the relations its manifest names and resolved the
rest out of the state it read, so recording that manifest claims relations
nothing built — and a model renamed since is recorded under a name only a full
build creates, breaking every later deferral until one repairs it.
Also: `publication_lock` parsed 16 hex digits as `i64`, which overflows for every
digest with the top bit set — half of them — collapsing those environments onto
one advisory key; and a failure to open the transaction returned without dropping
the objects already uploaded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: only a deployed dbt run publishes state, and key its objects per execution
A preview carries a caller-supplied `script_hash` into `runnable_id`
(`run_preview_script`), so the version guard alone let anyone who may run a job
publish arbitrary content as a deployed script's deferral state. The job's KIND
is checked beside it now. Verified: a preview submitted with the deployed path
and hash builds and leaves the row untouched.
Object keys carry a per-execution nonce. Zombie recovery re-runs a job under its
own id, so keyed on that alone a second attempt overwrote the objects the first
attempt's committed row still named, then read those same keys back as displaced
and dropped them — leaving the row unreadable. The displaced set is also filtered
against this publication's own keys, so the invariant is stated rather than
re-derived from the key format.
A project-owned `profiles.yml` that templates its schema or database is refused a
deferral: dbt renders those and Windmill does not, so two renderings resolve to
one `relation_root` and would share one environment key. Plainly absent is left
alone — that is the adapter's default, which does not move.
The deferral log line now says the run publishes no state of its own, which was
otherwise invisible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: a templated profile location publishes no dbt state either, on every path
A `dbt_profile` resource is one block of the user's own `profiles.yml` copied
through unchanged, and `profile.schema` is written as given, so either can carry
a template dbt renders and this runtime does not — exactly as a project-owned
file can. Only the project-owned path detected it.
And the refusal now covers publication as well as deferral: a published template
would sit under a key a literal profile shares, so de-templating later would make
that stale manifest readable as the new location's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: recognise Jinja statement blocks as a rendered dbt profile location
dbt renders a profile through Jinja, so `{% if env_var('ENV') == 'prod' %}…{% endif %}`
moves a schema exactly as an `env_var()` substitution does — and only `{{` was
detected, so such a profile published and deferred under one environment key for
every rendering. One predicate now serves both profile paths, with a test for
each delimiter.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: a dbt state read outruns successive publications rather than one
The loader re-read once, which answers a single publication overtaking it: a
reader takes no lock and the advisory lock is released before the displaced
objects are dropped, so back-to-back publications could each overtake the same
read and the second was reported as a missing object. It now re-reads for as long
as the row keeps MOVING, bounded, and reports only when an unmoved row's objects
are genuinely gone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: a dbt state read outruns successive publications, not one
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: name both ways a dbt state read can fail
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: length-prefix the dbt environment key's components
A dbt target name and a schema are both the user's own strings, so joining them
on `|` let one component spell another tuple's key: `prod|analytics` + `scratch`
and `prod` + `analytics|scratch` were one environment, and a profile moving
between them read as the same one rather than as one nothing has published — the
collision the key exists to prevent. The schema and database are also taken apart
now rather than through `relation_root`'s own join, so neither can absorb the
other's delimiter.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: name the dbt environment in words where a message shows it
The key is length-prefixed for storage, which is not something to put in front of
a caller: the "nothing published yet" refusal now reads "warehouse `main`, target
`prod`, relations in `dbt_wh_defer.analytics`". The worked example of the encoding
also miscounted a component.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: delete a script version in the transaction that cleans up after it
`delete_script_by_hash` soft-deleted through the pool, committing before the
cleanup that follows it in `tx`. In that window the path has no live version, so
a concurrent deploy can take it — and `clear_dbt_script_state_if_path_retired`
then finds that new script live, keeps the deleted project's dbt state, and
leaves the replacement able to defer through its manifest. The update moves into
the same transaction, which is what `archive_script_by_hash` beside it already
does.
The retirement guard itself was pinned by nothing: the existing test moved the
only row away before calling the conditional clear, so it could not fail.
`state_goes_only_once_no_live_version_is_left` covers both directions — a second
live version keeps the state, the last one leaving takes it — and fails if the
predicate is inverted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: archive a script by path in the transaction that cleans up after it
The last of the four routes still writing outside its own cleanup transaction.
Archived on its own, a cleanup that then fails leaves dbt state at a path no live
version occupies, and whatever is created there next can defer through it. The
by-hash archive and both deletes already take their write in `tx`; this makes the
set uniform.
Two comments beside those clears still called the state the RETRY state alone,
which the rename made false — they cover both halves now — and the merged
verification list had two `11.`, main's #10978 having inserted an item above it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: refuse a dbt state selector the engines resolve inconsistently
`state:`, `result:` and `source_status:` selectors resolve against the
artifacts in `--state`, which only a deferring run is handed. The engines
disagree about what happens without one, and two of the three disagree
silently: dbt-core 1.x raises, but dbt-sa-cli 2.x and fusion read a missing
state as an empty one and exit 0, so `state:modified` builds nothing and
`state:new` builds the whole project, each reporting success.
Refuse them up front instead, naming `defer`. From the descriptor they are
refused outright, since that selection also decides which nodes the script
owns and the deploy resolves it with no state at all.
`source_status:` is refused under any setting: it compares `sources.json`,
which no run publishes here.
A caller's selection is now allowed to match nothing, which is what
`state:modified+` returns when nothing changed since the published state. It
is stored as that run's own snapshot and never becomes what the script owns,
so the ownership-wipe the refusal guarded against cannot happen. The
descriptor's selection still may not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: refuse a dbt result selector the published state cannot answer
Round 18 findings.
Codex P1: `defer` alone was enough to allow a `result:` selector, but a build
recovered by node retry publishes a manifest with no `run_results.json` — the
only file such a selector reads. dbt-core then raises an internal error and the
Rust engines match nothing and exit 0. The deferral now reports whether the
state carries results, and a `result:` selection against one that does not is
refused, naming the run that published it.
Claude P2: a `parse` returns before `defer` is read, so its deferral is always
absent and "turn `defer` on" was advice that led nowhere. The check now
distinguishes a run that could defer from a command that never does, and the
parse path says so.
Codex P2 / Claude P2: the roadmap still listed `state:modified` as out of scope
while the same file documented it as working. Narrowed both that line and the
scope list to the slim-CI work that genuinely remains.
Also pins the invariant the relaxed empty-selection guard rests on: an
overridden selection must not publish ownership, or an empty caller selection
would wipe the script's graph.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: exempt an empty dbt selection by method, not by who chose it
Round 19 findings.
Codex P1: the empty-selection exemption keyed on whether the caller overrode
the selection, so a misspelled model name resolved to nothing, passed the guard
and reported a build that did its work. Key it on the selector instead: only a
`state:` or `result:` method may match nothing, its empty answer being a real
one. Every other selection matching nothing is refused again, from a run as
from the descriptor, each with the message that applies to it.
Claude P2: the spec still described a node-retry-recovered publication as one
where `result:` selectors merely lose their input, which the previous commit
stopped being true, and the section stating the selector rules recorded neither
the `result:`-without-results refusal nor the `parse` one. Both written down.
Also drops the refusal's claim that the publishing run WAS recovered by node
retry: an unreadable file reaches the same absent-results state, and the remedy
is the same either way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: record why an exempted empty dbt selection cannot wipe the graph
The safety argument left with the origin-based condition it justified. Under
the method-based one it is a consequence of the descriptor refusal in
check_state_selectors, two hops from this site, so state it here: relaxing that
refusal would let a descriptor-narrowed `state:modified+` reach the exemption
and be ingested as owning nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: let `// materialize` declare a `dbt://` warehouse-relation write
`// materialize manual dbt://<warehouse>/<schema>/<name>` lets an ingestion
script in any language declare that it writes a warehouse relation, so it and
the dbt model reading that relation land on one asset node instead of two
disconnected pictures. `manual` is the only mode a warehouse target has —
nothing generates warehouse DDL — and the non-`manual` spelling is refused
rather than silently degraded. The `<warehouse>` segment is resolved against
the workspace's configured warehouses, like a descriptor's `profile.warehouse`.
The run records the same `materialized_partition` row a DuckLake target does,
from the generic job path rather than an executor: the DuckLake write engine is
DuckDB's, this declaration is anyone's.
With a non-dbt producer now possible, the blanket deploy-time refusal of
`# on dbt://<relation>` narrows to the shape that still cannot fire — every
writer of the relation being a dbt script, since a dbt run does not dispatch.
"Nothing produces it yet" stays accepted, as for every other asset kind, so
deploy order does not matter. A dbt script may not subscribe at all: its graph
ingest clears its own `dbt://` trigger rows. The one ordering the deploy cannot
catch — a subscription accepted before any producer, then claimed by a dbt
project — is named in that project's deploy log.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rw1WrKeRRzyYHjfkuB83ek
* fix: address review — preview stamping, stale producer set, public doc
Three findings from the local review round:
- Record the warehouse write only for a DEPLOYED script job. The annotation is
a deploy-time contract (`manual`, three segments, a configured warehouse)
checked where write access to the path is also required; honouring it in a
preview, hub or inline-flow body let `jobs:run` alone restamp any relation's
last writer from a script that never touched it.
- Exclude the deploying script's own rows from the producer set. Read
committed, they describe the version being replaced, so a script dropping its
`// materialize` while adding a subscription counted itself as the producer
that would wake it and committed a dormant edge. It could not be that
producer anyway — the dispatcher skips self-loops.
- `AssetKind::Dbt`'s doc no longer claims dbt is the exclusive producer of a
warehouse relation, on both the types and the parser enum.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: review round 1 — dbt-script materialize, set-form rule, doc
- Refuse `// materialize` on a dbt script, the producer half of the rule the
trigger loop already applies to `// on`: the graph ingest republishes that
path's asset rows wholesale, so a declared write is wiped by the deploy that
accepted it while its runs keep stamping the relation.
- `dormant_dbt_subscriptions` now spells the same predicate its singular sibling
does: the producer set has to be non-empty (nothing produces it yet is deploy
order, not a dormant edge) and excludes the subscriber's own path (a script
never wakes itself). Both divergences are pinned by tests.
- The docs no longer claim the dbt deploy log covers a native producer that drops
its `// materialize`; it does not, and nothing else reports that case.
- An integration test over the deploy contract, since only a real deploy proves
the handler feeds `sole_dbt_producer` the canonical key `asset.path` holds —
the spelling that has to agree across the materialize target, the `// on` ref
and the refusal that joins them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: qualify the any-language claim, and pin the dbt-script refusal
`AssetKind::Dbt`'s contract (both enums), the two runtime guides and the deploy
comment said a script of any language may declare a `dbt://` write, which the
dbt-script refusal added last round contradicts. They now say "any language but
dbt's own", with the reason: a project's writes are read from its manifest.
The deploy-contract integration test covers that refusal for both annotations.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: teach the pipeline AI guidance the warehouse-relation target
The pipeline prompt (both sources, plus the regenerated bundle) told the model
`// materialize` is DuckDB-only and rejected on any other target, which now
steers users away from the very thing this PR adds. It distinguishes the managed
DuckLake write, still DuckDB-only, from the warehouse-relation declaration any
language but dbt's own may make.
`dbt_manifest.rs`'s module doc carried the same "the only thing that creates one"
overclaim the other four sites lost last commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: draw an explicit dbt:// subscription on the canvas
The editor suppressed every `// on dbt://…` overlay, which was right while the
deploy refused all of them. It now refuses only a relation dbt alone builds, so
the suppression hid the author's own annotation for exactly the case this PR
adds — a subscription woken by a native `// materialize manual dbt://…`
producer. The deploy stays the gate.
Also the two stale claims round 4 named: the live pipeline prompt dropped the
dbt-script exception the base prompt carries, and the doc's e2e requirements
still said every `dbt://` subscription is refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: refuse `// data_test` beside a `dbt://` materialize target
`// data_test` checks are verifier probes the DuckDB executor splices around a
managed write. A warehouse relation is written by the script itself, in any
language, so nothing would run them — and unlike the DuckLake `manual` case,
which at least fails loudly in that executor, a declarer in another language
deployed green with its data-quality assertions silently skipped.
Covered in the deploy-contract test and documented beside the annotation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: exclude a renamed producer from the sole-dbt producer set
The producer set already excluded the deploying script's own path, because its
committed rows describe the version being replaced. Under a rename the write
sits at the OLD path — still committed, and removed by the same uncommitted
transaction — so a producer renamed while it drops its `// materialize` and adds
`// on dbt://…` still counted as the producer that would wake it, and committed
a dormant edge.
The deploy-contract test covers it: without the exclusion the rename deploys
201 instead of being refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test: take the rename test's parent hash from the create response
`format!("{:x}", …)` over the stored i64 drops leading zeros, while
`ScriptHash`'s deserializer hex-decodes and demands 8 bytes — so a hash below
2^60 would 422 the request instead of reaching the refusal it asserts on, on
roughly one in sixteen spellings of that script body. The create response
already carries the zero-padded form, as the rest of the suite uses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: state the concurrent-ingest interleaving honestly
`sole_dbt_producer`'s doc claimed the concurrent-deploy race only ever resolves
toward refusing. It does when the uncommitted producer is native; when it is the
dbt ingest, the check sees an empty producer set and accepts, and if that ingest
then commits and runs its warning query before the subscriber's trigger row
lands, neither side reports the dormant edge.
Not serialized: the two would have to share a per-relation lock, and the ingest
takes `script … FOR UPDATE` before its own advisory lock, so a deploy holding
relation locks first inverts that order into a cross-subsystem deadlock — a worse
failure than the cosmetic edge. Recorded beside the other orphaning the deploy
cannot catch, with the bound both share: the next deploy of that project warns.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: refuse a `dbt://` subscription that is not a whole relation
`# on dbt://main/analytics` deployed and persisted a trigger row. Every producer
spells `<warehouse>/<schema>/<name>` — the manifest ingest derives it from
`relation_name`, a `// materialize` target is checked against it — so a partial
one is an edge nothing can ever wake, which is what the dbt-only refusal exists
to prevent.
The shape now has one definition (`is_full_relation_path`) that both halves of
the deploy ask, rather than a segment count spelled twice: a subscription and a
write that disagreed would refuse and accept the same string.
Also rewrites the canvas test's comment as a current constraint per AGENTS.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: hold both halves of the deploy to one `dbt://` relation validator
A subscription checked the relation's shape but not its warehouse, so
`# on dbt://<unconfigured>/<schema>/<name>` deployed and persisted a trigger row
for something no producer can ever write: the write side refuses that exact
string, and a dbt project's `profile.warehouse` resolves against the same config,
so no later deploy fixes it and the dormant-edge warning cannot report it either.
The shape rule and the warehouse rule now live in one `validate_dbt_relation`
that both halves call, rather than being spelled per site — the previous two
rounds each closed one half of one rule, which is the drift that invites.
Also moves the parser test out from between a comment and the test it documents,
and names both refusals in the doc's list.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: drop the subscription-only clause from the shared refusal message
"so nothing can produce it" reads backwards on the `// materialize` side, which
is the producer. The remaining sentence says what is wrong on both.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: bound a `dbt://` relation by the asset-path column in the shared validator
`asset.path` is VARCHAR(255) and the manifest ingest drops a relation that
outgrows it rather than failing the whole graph, so past the column no producer
row can exist on either side. `script_trigger.trigger_ref` is unbounded text, so
an overlong subscription deployed and stayed dormant for good; an overlong write
reached Postgres and failed the deploy on a `value too long` instead of a message.
Both now refuse in the validator the two halves share, against the ingest's own
constant. The integration case computes the ref from that constant so it cannot
drift back under the bound.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: report a warehouse-lookup failure as the failure it is, and correct the boundary
`dbt_warehouse_exists` fails three ways — no such warehouse, the query itself,
and a setting with no `resource_path` — and all three became a 400 blaming the
user's warehouse name. A pool timeout mid-deploy told a retrying sync that a
transient server error was a permanent client one. Only `NotFound` is the
annotation's fault now.
The known-boundary paragraph claimed a flow-runner run still cascades. It does
not: it is routed by `flow_step_id`, which `is_eligible_kind` rejects, as
`asset_trigger_dispatch.rs` pins. Recording and cascading are decided separately,
so the paragraph now names all three routes rather than merging two of them — and
the row it omitted, an ordinary flow step, which records and never cascades.
E2E item 7 said "deployable" where the rule is "wakeable": with only the dbt
project reading the relation the producer set is empty, which deploys fine.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: correct two rationales the last commit got wrong
`Error::SqlErr` already maps to 400 in this codebase, so the query case's status
was never the thing at stake. What the `NotFound` match earns is that a query
failure and a malformed setting stop being described as an unconfigured warehouse
name, and that the malformed-setting `InternalErr` reaches its own 500 instead of
being flattened.
And a flow step is two shapes, not one: a step running a deployed script is a
`Script` job that records and never cascades, while a step with an inline body is
`FlowScript`, which the recording guard excludes along with previews.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: warn about dormant subscriptions from the run that publishes ownership too
A run whose static descriptor finds its profile moved re-ingests the version's
graph and republishes path ownership, exactly as a deploy does — so it can be
what leaves a subscription accepted while the relation had no producer with dbt
as its only one. That path discarded `persist_ingest`'s result and emitted no
warning, which also made the doc's enumeration of unreported orphanings wrong.
Both ownership-publishing points warn now. An agent worker still cannot: it
reaches these tables only through the API and its ingest publishes without
reading back, which the doc now says.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: an agent run publishes no ownership, and the warning has two callers
The agent-worker sentence called it an exception that publishes ownership without
warning. It publishes none: `Connection::Http` forces per-run models, and
`publishes_ownership()` is the negation of that, so an agent stores a job-pinned
snapshot and leaves workspace ownership with the deployed graph — it cannot orphan
a subscription at all.
`warn_dormant_subscribers`' own doc still named the deploy log as the only place
the warning shows, one commit after it gained its second caller.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: stop the managed-write rule from contradicting the dbt:// target
The sentence after the warehouse-relation paragraph says `// materialize` means
the runtime writes the table for you and the body is a bare SELECT. That is the
managed DuckLake rule, written before a `dbt://` target existed, and unqualified
it tells the model the opposite of what the paragraph above it just said — a
model following the more prominent one emits a SELECT for a warehouse relation,
which deploys and then writes nothing.
Both prompt sources now scope it, and both name the `// data_test` refusal beside
a `dbt://` target, which the badge list advertised without the caveat.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cli): keep permissioned_as on single-item push, as sync push does
* fix(cli): resolve syncBehavior from the target workspace, not the branch alone
* refactor(cli): share the workspace-name resolution between sync and single-item push
* test(cli): import the moved workspace-name helper from its new home
* feat: show the new-tab icon on a chat path pill while the modifier is held
Fixes WIN-2477
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAa9nNcYxDN3qAZPrYg4Lq
* fix: read the new-tab modifier in the capture phase
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAa9nNcYxDN3qAZPrYg4Lq
* refactor: track the new-tab modifier only while a pill is hovered
The window key listeners were installed at import time and never removed, so
every page that loaded the module paid for them whether or not a pill existed.
They now attach on mouseenter and detach on mouseleave or destroy, which is the
only window in which the answer is read.
Seeding the flag from the hover event also removes the limitation the previous
version documented: a mouse event carries the same modifier flags as a key
event, so a modifier held before the pointer arrived, or while this window was
unfocused, now reads correctly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAa9nNcYxDN3qAZPrYg4Lq
* refactor: export the new-tab modifier as a read-only view
`newTabModifier` handed every consumer a writable handle on module-global
state, so any of them could drive the icon of every pill on the page. The
getter form is what frontend/AGENTS.md prescribes for shared reactive state.
Tearing each attachment down in the test's afterEach as well: the module state
and its window listeners outlive the DOM, so emptying the body left `held` and
the hovered node set for the following case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAa9nNcYxDN3qAZPrYg4Lq
* refactor: only track the modifier for pills whose icon can change
The attachment went on every path pill, so hovering a drawer or plain-link pill
installed three window listeners for a flag its icon never reads. Only a
preview pill can flip, so only it gets them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAa9nNcYxDN3qAZPrYg4Lq
* fix: re-read the new-tab modifier from pointer movement
A modifier held across a keyboard app switch was cleared by the blur and never
restored: the key was down the whole time so no keydown arrived on the way
back, and the pointer parked on the pill fired no fresh mouseenter either. The
pill then showed the panel icon while the click would have opened a tab.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAa9nNcYxDN3qAZPrYg4Lq
* refactor: give each pill its own modifier state
The shared module state forced a node-identity guard: one hovered element owned
the window listeners, so a pill destroyed elsewhere in the transcript had to be
stopped from tearing them down. A factory per pill removes the guard, its test
case, and the whole class of cross-instance interference, and narrows re-renders
to the hovered pill instead of every preview pill on screen.
Listener teardown now goes through AbortController signals, so leaving a pill
drops the whole set at once rather than through a remove list that has to mirror
every option exactly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAa9nNcYxDN3qAZPrYg4Lq
* fix: abort the previous hover controller on re-entry
A second mouseenter with no mouseleave between replaced the controller without
aborting it, so the four listeners registered under the first signal outlived
even the element's destruction: neither leave nor the destroy path held a
reference to reach them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAa9nNcYxDN3qAZPrYg4Lq
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: write and read python job files as utf-8, not the platform locale
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ErBZtEpLpkFi1Y1W6eNZBE
* refactor: trim the PYTHON_UTF8_ENVS comment to the 4-line limit
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ErBZtEpLpkFi1Y1W6eNZBE
* chore: bump ee ref for the python runner-group utf8 companion
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ErBZtEpLpkFi1Y1W6eNZBE
* chore: update ee-repo-ref to d33ea730c550cdbc7d050aeb6d40dcef3d134e07
This commit updates the EE repository reference after PR #782 was merged in windmill-ee-private.
Previous ee-repo-ref: c8318661f8d91da9172a3c2dca050b70ba7afda2
New ee-repo-ref: d33ea730c550cdbc7d050aeb6d40dcef3d134e07
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix(oauth): show the account chooser on Google/Microsoft login
Without `prompt=select_account`, Google and Microsoft silently reuse the single
active browser session, so a user with more than one account has no way to pick
which one to sign in with.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0196aV8v36ukoQcD7L2scvmH
* chore: pin ee ref for the oauth login extra_params fix
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0196aV8v36ukoQcD7L2scvmH
* fix(oauth): only ask for the account chooser on an explicit login click
The login page now sends `user_initiated=true` when someone clicks a
provider button, and the backend applies the provider's `extra_params`
only for those requests.
Someone whose browser holds a single Google session whose email is
already registered under a different login type hits
"an user with the email associated to this login exists but with a
different login type" and, with no account chooser, has no way to offer
a different account. The chooser belongs on that click.
It does not belong on the `auto_login_provider` redirect, whose whole
purpose is to sign a public-app or approval-page visitor in without
interaction.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0196aV8v36ukoQcD7L2scvmH
* fix(oauth): make the account chooser the default, not the opt-in
The login page now flags only the `auto_login_provider` redirect, with
`auto=true`; every other login — a click on a provider button, or the
endpoint opened as a plain URL — gets the provider's extra params.
`/api/oauth/login/*` is whitelisted in `public_app_layer` and reachable
directly, so an opt-in flag would silently drop the account chooser for
every caller that is not our own button.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0196aV8v36ukoQcD7L2scvmH
* chore: update ee-repo-ref to f5d6b6b8dd00b0141308337ac97f4685781f2b1c
This commit updates the EE repository reference after PR #776 was merged in windmill-ee-private.
Previous ee-repo-ref: 5684bb0f63dce08d6ce9ab0183072c8b4fce4b2e
New ee-repo-ref: f5d6b6b8dd00b0141308337ac97f4685781f2b1c
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 (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): render ordered lists in markdown descriptions
`GfmMarkdown` defaulted to `prose-xs`, which Tailwind Typography does not
define — the class only ever matched four hand-rolled rules in app.css, all
scoped to `ul`. Every surface on that default (script and flow descriptions,
flow-graph notes, markdown job results) therefore rendered `<ol>` with
Preflight's `list-style: none` and no typography at all: no numbers, no
heading or paragraph rhythm.
Route the default through the shared `markdownProse` stacks instead, and cut
the app.css list rules down to the dash glyph so ordered and unordered lists
share Tailwind Typography's indentation and rhythm.
Fixes#10971
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S6G5gDXJnm6uqch4uCPkPE
* fix(frontend): address review nits on the markdown prose fix
- default `GfmMarkdown` to the `sm` stack rather than `xs`: the AI-agent tool
Message pane takes the default and has no ancestor font size, so `xs` left it
smaller than its own label. The group note, whose wrapper is `text-2xs`, opts
down explicitly.
- regenerate `static/tailwind_full.css`, which raw apps are served and which
still carried the deleted list rules.
- correct the marker-color rationale: the typography config already maps markers
to tertiary, so the rule steps them up rather than rescuing them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S6G5gDXJnm6uqch4uCPkPE
* fix(frontend): make the note color override an arbitrary value
`text-inherit` is not generated: this config replaces the Tailwind color palette
outright and defines no `inherit` key, so `[&_*]:!text-inherit` compiled to
nothing and notes still rendered in the prose stack's `text-primary`. Verified in
the browser: a yellow note's list items now compute to `text-yellow-900`, matching
the wrapper and the edit-mode textarea, in both themes.
Also drop the `static/tailwind_full.css` regeneration. That file was generated with
tailwind 3.4.1 against a config predating the typography theme overrides; rebuilding
it today sweeps in 250KB of unrelated churn and would flip every raw app's `.prose`
palette from stock gray to Windmill tokens. Its staleness predates this PR and is
its own change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S6G5gDXJnm6uqch4uCPkPE
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: guest JWT entry for embedded apps (jwt_guest_)
A second way in for a guest, alongside the signed-in guest session: a JWT the
embedding customer's backend mints and signs, verified per request against a
per-workspace key (a PEM public key or a JWKS URL), resolving to the same
seatless guest identity confined to the one app its app_path claim names.
Bearer prefix jwt_guest_, stateless (no token row). See PR #10954.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: surface guest JWT as the embed method in the app deploy drawer
The deploy drawer explained the secret-URL embed but not the guest JWT path, so
the primary way to embed an app for a customer's own authenticated users was
undiscoverable. For a guest-mode app with guests enabled, show how to mint a
`jwt_guest_` token and append `guest.<jwt>` to the app URL, with a copyable
iframe template pre-filled with this app's workspace_id and app_path, and a note
that new guest emails are refused past the instance's free allowance (the live
count is shown just above).
Also log a guest JWT allowance refusal at warn, not info: the caller gets a bare
401 (the reason must not leak to an unauthenticated caller), so the log is the
admin's signal that the instance hit its guest cap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: correct the guest JWT minting instructions in the embed block
The block said "sign it with the workspace's guest JWT key", but that setting
holds the public verification key. Clarify the keypair relationship (configure
the public key or a JWKS URL in the workspace; sign with the matching private
key), name the accepted algorithms (RS/PS/ES; HS* refused), and keep the
required claims, so an embedder knows how to actually mint the token.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: fall back to the instance JWT issuer for guest verification (off on cloud)
A workspace with no guest key of its own now verifies guest JWTs against the
instance issuer (JWT_EXT_JWKS_URL, already used by jwt_ext_), so an operator
running one issuer configures it once. Verification and the guest grant are CE;
granting a full login from that issuer stays EE (jwt_ext_, unchanged). Disabled
under CLOUD_HOSTED, where one instance issuer must not be trusted to mint guests
in every tenant's workspace — there the per-workspace key is the only source,
which also stays the override everywhere. The workspace settings note (hidden on
cloud) explains the fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: embed instructions cover both the workspace key and instance issuer
The embed block said to set the workspace's guest JWT key; now it says Windmill
verifies against the workspace key or, off cloud, the instance issuer
(JWT_EXT_JWKS_URL) when no workspace key is set. The instance clause is hidden
under isCloudHosted().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: show the guest JWT embed block only when Embed is toggled
It belongs with the iframe snippet, not the plain-URL view, so gate it on
embedMode alongside the guest-mode / guests-enabled checks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: trust the instance issuer in the guest fallback; refresh stale docs
P1 (CI review): the fallback wrapped JWT_EXT_JWKS_URL as a workspace JwksUrl, so
it hit validate_guest_jwks_url and was refused for http/private issuers unless
ALLOW_PRIVATE_GUEST_JWKS_URLS was also set — a self-hosted internal issuer that
works for jwt_ext_ failed for guests, though the UI says setting the env var is
enough. fetch_jwks now fetches the instance issuer without the https/private
restriction (matching the jwt_ext_ loader; it stays operator-trusted), while a
workspace-admin URL is validated and pinned as before. All the size/key/URL
bounds still apply to both.
P2 (CI review): refresh the stale docs that said a missing workspace key always
refuses a guest JWT — the module, bearer, key-source, and EditGuestJwtKey field
docs now describe the workspace key with the off-cloud instance-issuer fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: fetch the trusted instance issuer like the jwt_ext_ loader
P1 (CI review): the instance-issuer fetch skipped SSRF validation but still
disabled redirects and default cert validation, so an instance issuer that works
for jwt_ext_ through a redirect or an operator-approved self-signed cert failed
the guest fallback. Fetch it with HTTP_CLIENT_PERMISSIVE (follows redirects,
honors ACCEPT_INVALID_CERTS) — the same behavior jwt_ext_ has — while a
workspace-admin URL stays validated, DNS-pinned and redirect-free. The body size
cap still bounds both.
P2 (CI review): the WorkspaceSettings field doc still said None/None means no JWT
guests; it now names the off-cloud instance-issuer fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: schema summary + OpenAPI cover the guest JWT columns and fallback
P2 (CI review): summarized_schema.txt was missing guest_activity.jwt_entry and
the two workspace_settings guest-JWT key columns (required by docs/validation.md
after a schema change). The edit_guest_jwt_key OpenAPI description now notes that
clearing the workspace key falls back to the instance issuer (JWT_EXT_JWKS_URL)
off cloud rather than necessarily stopping guest JWTs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: keep JWKS single-flight locks in a self-cleaning map, not a bounded cache
P1 (CI review): JWKS_FETCH_LOCKS was a 200-entry quick_cache. Past 200 cold URLs
it can evict a lock whose fetch is still in flight; the next request for that URL
then mints a fresh lock and starts a second fetch, so cycling configured
workspaces defeats single-flight and can storm the issuers. Replace it with a
plain map guarded by a JwksFetchLock RAII handle that removes each entry once its
last holder drops, so the map only ever holds the fetches in flight and never
evicts an in-flight lock. Add a unit test pinning the shared-lock and
self-cleaning invariants.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: update ee-repo-ref to c2270eb5fe2d9f0968253e6b460c33186363f4e7
This commit updates the EE repository reference after PR #773 was merged in windmill-ee-private.
Previous ee-repo-ref: 5a1d9dee34159512c0823fddcd3d096490edbcce
New ee-repo-ref: c2270eb5fe2d9f0968253e6b460c33186363f4e7
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>
* fix: bump the ee ref past the seats_consumed test arity break
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VwxTTC4GsAANBjEZki2cft
* chore: update ee-repo-ref to fb1c5c109846d6c47aff70ab6cc631f4fd773678
This commit updates the EE repository reference after PR #781 was merged in windmill-ee-private.
Previous ee-repo-ref: d197b7b1c76e2aa7cde6cef2e2d9556607cce4c6
New ee-repo-ref: fb1c5c109846d6c47aff70ab6cc631f4fd773678
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat(frontend): group the AI agent step form and edit saved agents in a modal
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat: edit a saved AI agent through its own resource draft
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor: drop the agent fork-for-edit session now that edits live in a draft
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat: edit ai_agent resources from the resources page with the agent editor
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: send a standalone agent's brain from the module when testing a step
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: keep the agent draft faithful to the resource it deploys to
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: add the sqlx cache entry for the eval subject rename
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor: share the module insert between the graph and the agent editor
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(frontend): open evals inside the agent editor, actions in its header
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(frontend): add tools from the agent editor and lighten its test pane
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): open an ai_agent deep link in the agent editor
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): drop the failed result badge on a step that never ran
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(frontend): head the agent editor's levels with a back control
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(frontend): drop connect and fill inputs from the agent editor
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(frontend): lighten the agent editor's run panel
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): stop a nested agent tool's config reading as AI-filled
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(frontend): offer only AI or static on an agent tool's inputs
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(frontend): give a saved agent's tool editor a static-only surface
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(frontend): open an agent tool in a drawer beside the agent
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(frontend): hide unset agent config in the run form
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(frontend): share the input forms' pickers and s3 lookup
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(frontend): drop a dead agent-editor export and fix two stale comments
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): reach an ai_agent's resource-level settings and copilot
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): open an ai_agent's resource view as JSON, not the generic form
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): address review findings on the agent editor's draft and streaming
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): close the agent editor on a version restore, as the resource editor does
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): stop the provider picker auto-writing a kind, and clear review nits
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(frontend): drop the fork-for-edit leftovers from the agent card
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): mount the agent editor in the dev flow editor and guard the deep-link race
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): deploy the agent config that was submitted, and refuse one no run could use
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(frontend): build the agent editor's rows from the design-system button
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): keep a draft-only agent's draft, and let a blank MCP summary deploy
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): guard read-only agents, incomplete MCP tools and duplicate editor mounts
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: read-only agent editor, linked-card refresh, atomic eval rename
* fix: eval rename needs the privileged pool, per-workspace write access
* fix(frontend): drop the agent editor target when its mount goes away
* refactor: drop the agent rename work from this PR, unban the bindable defaults
* fix(frontend): refuse a renaming deploy and drop the copilot from static-only fields
* fix(frontend): mirror the worker's streaming rule and scope agent writes to their target
* fix(frontend): read runtime streaming as off and reset the drawer's json view
* fix(frontend): read an unsettled output_type as non-streaming too
* fix(frontend): let the showing modal claim an agent opened from inside it
* fix(frontend): keep in-flight edits, tool replacements and every linked step in sync
* fix(frontend): keep attachments in the run form and bind the agent ref to its tools
* fix(frontend): preview the agent as authored and re-evaluate step args on run
* fix(frontend): scope agent-editor ownership to the flow's workspace
* fix(frontend): drop the tool drill-in where there is no graph to select on
* fix(frontend): require a provider kind and keep one resource editor open at a time
* fix(frontend): keep legacy nulls, static-only text literal, and the handover anchor
* test(worker): pin the agent streaming default
* fix(frontend): let an AI-fillable input be switched to static
* fix(frontend): report agent editor background failures instead of floating them
* fix(frontend): keep the version pane's path alive while the editor closes
* fix(frontend): clear the anchor-keep flag at the start of each drawer session
* fix(frontend): preview the agent without its synthetic path, refresh the baseline on external writes
* refactor(frontend): drop the unverifiable baseline refresh, state the synthetic-path rule
* fix(frontend): keep the synthetic path out of agent tool test runs too
* refactor(frontend): host the agent editor under the agent's own path
* fix(frontend): mark an agent editor's host explicitly instead of inferring it from the path
* fix(frontend): discard linked-agent responses from before a deploy
* fix(frontend): keep a flow mount from claiming an agent editor's nested target
* feat(frontend): keep an agent used as a tool inside the agent being edited
* fix(frontend): reserve the agent editor's root module id
* docs(frontend): record why the agent editor previews under the agent's path
* fix(frontend): refuse to open or deploy a resource that is not an agent
* docs(frontend): put the scope-migration comment on the function it describes
* fix(frontend): refuse an agent path whose resource type is not proven
* fix(frontend): recheck the resource type before deploying, and keep expressions off static-only inputs
* fix(frontend): lazy-load the agent editor and slide its levels like the evals pane
* refactor: drop unreachable non-list tools check from agent deploy
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): clear text-only agent fields on image output, reserve the root id
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): keep the agent editor usable for a non-list tools value
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): stop the parked eval run list from taking arrow keys
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): report a non-list tools value on deploy instead of throwing
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): keep temperature editable for image output
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): skip non-object tool entries when rendering an agent
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): guard tool entry reads instead of copying the tool array
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): key tool rows by position so duplicate ids render
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix: tell duckdb scripts which extension to name when autoload hits the fence
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: update ee-repo-ref to b9aeffa83f0e601f123c7eab536b235719786da1
This commit updates the EE repository reference after PR #778 was merged in windmill-ee-private.
Previous ee-repo-ref: fd196f99e22205c69946870997dadd921847cc97
New ee-repo-ref: b9aeffa83f0e601f123c7eab536b235719786da1
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* chore: stop denying reads of secret files in claude settings
Any Read() deny rule makes Claude Code resolve the file operands of every
Bash command that reads files. A path it cannot resolve, such as one that
follows a cd into a directory the analyzer does not track, escalates to a
permission prompt even under bypassPermissions. A plain recursive grep in
the repo root escalates too, because it could reach .env.
Drop the read rules and widen the write rules to cover the same files, so
secrets still cannot be written through Edit, Write, or a shell redirect.
Reads of those files are no longer blocked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNCupPk2yewQT1JMNjkV8M
* fix: keep the sso group reconciler alive in oauth2-less builds
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W24T1FjQXQ87AoeC3UxWWC
* chore: update ee-repo-ref to d6297e6844dc2aab4745fce328e32ccab508969f
This commit updates the EE repository reference after PR #777 was merged in windmill-ee-private.
Previous ee-repo-ref: eec88486fb2df0ba15998ef285f52fc67af90b1e
New ee-repo-ref: d6297e6844dc2aab4745fce328e32ccab508969f
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Any Read() deny rule makes Claude Code resolve the file operands of every
Bash command that reads files. A path it cannot resolve, such as one that
follows a cd into a directory the analyzer does not track, escalates to a
permission prompt even under bypassPermissions. A plain recursive grep in
the repo root escalates too, because it could reach .env.
Drop the read rules and widen the write rules to cover the same files, so
secrets still cannot be written through Edit, Write, or a shell redirect.
Reads of those files are no longer blocked.
Claude-Session: https://claude.ai/code/session_01RNCupPk2yewQT1JMNjkV8M
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: add sso_groups_claim setting for login-time instance group sync
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YESxWqzt959S6TY6vbc4eG
* chore: bump ee-repo-ref for the SSO groups claim reconcile
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YESxWqzt959S6TY6vbc4eG
* chore: update ee-repo-ref to 3b89bfc11314a326a191101cfe3ef65f6f7f82a8
This commit updates the EE repository reference after PR #774 was merged in windmill-ee-private.
Previous ee-repo-ref: e388527f9adbbe466fe050ca8d1d236ce3342bc3
New ee-repo-ref: 3b89bfc11314a326a191101cfe3ef65f6f7f82a8
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* docs: teach agents to pass a resource as $res:<path> in run arguments
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjRARL7JA7xm772iJP4mJk
* docs: extend run-argument rule to in-editor chats, fix run-as wording
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjRARL7JA7xm772iJP4mJk
* docs: tighten resource run-argument rule after review
- Drop the false rationale that "$var:" only works inside a resource value
from the write_variable description and its runtime rejection message; keep
the rule (a variable cannot reference itself).
- MCP resource-argument description: the title fallback renders "No title",
so say the title is only a label rather than that it can be empty. Guard the
real-newline fix with asserts in the existing enrichment test.
- Eval: assert the full "$res:f/evals/global/github_main" value as one prefix
so a wrong path with a right prefix fails.
- resources.md: narrow "a trigger's payload" to its configured static args.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjRARL7JA7xm772iJP4mJk
* docs: scope the run-argument rule to global chat, add an exact eval matcher
The ai_evals A/B on the two in-editor modes showed no effect: script mode
sonnet 5/5 both with and without the description, flow mode sonnet 5/5 and
haiku 5/5 on the baseline alone. A flow's input schema already carries
`format: resource-<type>`, so those modes have a signal global mode does not
give. Revert both files to keep the tool schemas free of a description that
buys nothing per iteration; global mode keeps it, where haiku goes 0/5 -> 5/5.
Add `stringEqualsAnyOf` to toolCallArgs and use it for the resource reference:
nothing in the eval resolves the value, so a prefix match accepted a near-miss
path like `$res:f/evals/global/github_main_backup`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjRARL7JA7xm772iJP4mJk
* docs: address cubic review — CLI wording, mock resource getter
- `-d --data` help on all four run/preview commands: give $res: and $var:
their own clauses instead of a parenthetical that read as if a resource
were a kind of variable.
- Mock backend: `getBenchmarkResource` now resolves AI-provider seeds as well
as plain ones, so it agrees with `existsResource` and `listResource` — both
report either kind, and a case that listed a resource and then read it by
path got a row it could not fetch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjRARL7JA7xm772iJP4mJk
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: let the import wizard reuse an existing workspace resource
The project import wizard always opened the create-resource drawer, so a
workspace that already had, say, an SMTP resource still ended up with a second
one. Step 4 now offers a choice: fill in a new resource as before, or pick an
existing one of the same type.
Picking an existing resource rewrites the deployed items to point at it and
then deletes the imported stub. The rewrite covers scripts, flows, apps, raw
apps and every workspace trigger kind, and holds two rules: it writes nothing
unless every referrer can be rewritten, and it only touches items under the
target folder.
Raw apps re-upload the bundle shipped in the project export instead of
rebuilding it, and the retarget refuses when the deployed sources have moved on
since the import — that bundle was built from the export's sources, so
re-uploading it over edited sources would revert them.
Adds `update` to the trigger-kind table for the eleven kinds whose service
takes a plain config body; schedule keeps its own branch because
updateSchedule takes a different shape.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj
* feat: only ask about resources the project actually points at
A project declares one resource per `resource-<type>` input schema as well as one
per `$res:` reference, so an app that pins `f/calendly/google_calendar` for a
script whose schema says `resource-gcal` ships an unreferenced `f/calendly/gcal`
alongside it. Step 4 listed both and asked you to fill in each.
Only the referenced ones have to hold a credential for the project to work. The
rest are still created — a standalone run picks from them in the argument picker
— but they no longer reach the checklist, and `resourceCount` counts the same
set so the wizard does not offer a fourth step that has nothing on it. Across
the twelve published hub projects this drops 9 of 19 rows, including three
non-credential input shapes in `typeform`.
Also fixes a miss in the retarget: a trigger holds its resource as a bare path in
its own `*_resource_path` field rather than as a `$res:` token, so a token-only
scan left it pointing at a stub that was then deleted. Detection now mirrors
`rewriteTriggerConfig` through a shared `referencesResourcePath`, which matches
the parsed structure rather than its serialization — keeping `f/proj/db` out of
`$res:f/proj/db_prod` as well.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj
* fix: refuse a resource retarget the scan or the rewriters cannot cover
Uncompiled trigger features 404 on their list route; that is the instance not
having the kind, not a listing that failed, so it no longer blocks every
retarget on a stock build. The `listSearch*` endpoints cap server-side with no
ordering and no pagination, so a full page is refused rather than read as the
whole workspace. An item that names the resource path outside a `$res:` token is
refused at plan time — no rewriter relocates it — and the trigger row keeps its
own `script_path` so a runnable sharing the path is not repointed. A raw app
whose sources the export cannot yield carries no entry at all, so the refusal
its comment promises actually fires. The reused row offers text instead of a
button that leads to a deleted resource.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj
* refactor: let an incomplete scan keep the stub instead of refusing the retarget
The scan behind "nothing is written unless every referrer can be rewritten"
cannot be proven complete: the listings come back capped, a trigger kind can
fail to list, and a reference can sit where no rewriter reaches. Gating the
whole run on that claim made every such case a refusal.
Rewriting an item onto the chosen resource is safe on its own — the item
resolves whether or not the stub survives — so only the delete needs the claim.
`planRetarget` now answers with the referrers it can move plus the gaps it
cannot account for, `applyRetarget` always moves the first set, and a gap keeps
the stub rather than stopping the run. A referrer outside the project's folder
is one of those gaps: the listings are workspace-wide, so it is seen for free,
it stays the user's own, and its existence is why the stub stays.
The outcome carries what moved and why the stub was kept, so the row settles to
the chosen resource either way and says when the placeholder is still there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj
* fix: preserve a retargeted item's deployed identity, and send back its own bundle
Every write here edits a deployed item in place, but none of them said so.
Without `preserve_on_behalf_of` the backend replaces the item's stored run
identity with whoever opened the wizard, and `updatePolicy(next, undefined)`
rebuilt an app's policy from nothing — dropping its sandbox rules and forcing
`execution_mode: publisher`, which puts a viewer app on the publisher's
identity even though the backend would otherwise have kept the deployed mode.
The policy is now recomputed from the deployed one, which is what the
triggerables rekeying actually needs.
The raw-app bundle no longer comes from the project export. The browser can
read a deployed bundle back — mint the app's public secret and fetch
`/apps/get_data/v/{secret}.{ext}`, the same route the Hub publish reads — so
the bundle sent back is the deployed one whoever last edited it. That removes
`ExportedAppFiles`, its plumbing through the setup step, `rawSourcesDiverged`,
and the two raw-app gaps: an app "edited since the import" is no longer a case
that exists.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj
* perf: carry the trigger row from the scan into its write
`rewriteTrigger` listed the whole kind again to find the row it had just read,
once per trigger — and for schedules a listing is itself a listing plus a
detail fetch per row. The scan already holds the row, so the referrer carries
it.
Pins two properties that nothing covered: the trigger update body leaves
`enabled` out, so pointing a trigger at a credential cannot also start it; and
a write that fails partway keeps the stub while reporting what had already
moved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj
* fix: keep unfilled resources out of the reuse chooser
The chooser offered every resource of the row's type except the ones this import
created, so a stub left behind by an earlier import of the same project showed up
as a credential to reuse. Pointing a project at another project's empty
placeholder is never the answer, and nothing downstream would have complained.
Candidates are now read back and the unfilled ones dropped, using the same test
the checklist uses to call one of the project's own resources blank. Past a cap
they are all offered rather than costing a request each: a workspace with that
many resources of the outstanding types is not the case this filters for.
Also drops the chooser's promise that the imported placeholder is removed. That
was true when the delete was unconditional; the stub is now kept whenever the
scan cannot account for everything, and the row says which happened once it has.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj
* fix: move a retargeted item's bundle and identity, and see the paths it spells out
Four gaps between what the retarget claimed and what it did.
A trigger states its run identity as `permissioned_as`, not the `on_behalf_of`
the other kinds use, and the backend keeps the row's value only when
`preserve_permissioned_as` says so. Without the pair, a trigger created under a
folder's `default_permissioned_as` started running as whoever picked the
credential.
A raw app's bundle is compiled from its sources, so a `$res:` a source spells out
is baked into it. The import rewrites that copy — `retargetProjectExport` runs
while `/bundle.js` is still one of `files` — but the retarget fetched the
deployed bundle after that split and sent it back untouched, then deleted the
stub the app still read. The fetched bundle is now rewritten too, and a path it
names any other way keeps the stub instead.
A script's content is one string, so the whole-string match that finds a bare
path in a flow or an app could not see one written inside it. `getResource("f/…")`
was invisible to both the scan, which then deleted the stub under it, and the
step-4 filter, which dropped the row so nobody was asked to fill it.
Trigger listings cap at the server's DEFAULT_PER_PAGE, which this table does not
page past. A full page is now read the way a full `listSearch*` page is: as a
listing that cannot account for the rest.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj
* fix: see a path a flow or app spells out, and name why an item did not move
The script scan was taught to see a resource path written inside code; flows and
apps were left on the whole-string test, which cannot. A flow whose inline module
runs `getResource("f/proj/db")`, or a raw app whose source does, was neither
rewritten nor recorded as a gap, so the stub was deleted while the deployed item
still read it. Reachable from the wizard, because the step-4 filter does see such
a reference and offers the row.
Both branches now use the same test as the script branch, and gap rather than
rewrite: the stub survives either way, so a `$res:` token in the same item still
resolves, and rewriting half an item would only make the plan and the write
disagree about what moved.
Each rewriter now says why it left an item alone instead of answering yes or no,
so a raw-app bundle that spells the path out is reported as a reference nothing
could move rather than as a concurrent edit.
Also corrects the resource-listing comment — `perPage` bounds the answer, the
route does not default to 30 — and asks the askable-resource question against the
export as published rather than the retargeted copy, so the step and the stepper
that decides whether to offer it give one answer. A path spelled out in code is
not retargeted, so only the raw export has its references and its resource paths
agreeing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj
* fix: a kept placeholder is still something to fill in
Reuse marked the row done and replaced its action with static text even when the
stub survived. A kept stub is empty and is still what every item the scan could
not move reads, so the step reported "You're all set" over a project running on a
placeholder, with no way back to filling it. Reachable from one hub project: a
raw app whose source spells the resource path out gaps everything, nothing is
rewritten, and the row went green anyway.
Such a row now stays outstanding, keeps its button, says which path items still
read, and re-checks on refresh so filling that placeholder in closes it.
Flows and apps also went back to being rewritten as well as gapped, matching what
the script branch already did — the reason given for skipping them was
contradicted by that branch, and a comment merely naming the path was enough to
strand an item's real `$res:` token on the stub.
Two things had to become precise for that to hold. What counts as rewritable is
now the presence of a `$res:` token rather than any reference, since a whole
string equal to the path is the unreachable case, not a movable one. And the
post-rewrite check reads tokens only: a path the item also spells out is the
plan's gap to record, and re-reading it at write time reported one item twice,
as both unmovable and changed underfoot. Writers now skip a write that would
change nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj
* fix: rewrite only the tokens, and let a filled placeholder close its row
The import's flow and app rewriters also remap a runnable's own path on an exact
match. That is right for the folder-wide map the import hands them, where every
path is moving. Here the map holds one entry, a resource path — and scripts,
flows and resources share a namespace, so a project shipping both a script and a
resource named `smtp` had the step calling it repointed at the credential.
Triggers were already guarded against exactly this; flows, apps and raw apps were
not. All three now rewrite the serialized value, which moves the tokens and
leaves every path alone.
A kept placeholder that the user then fills in now closes its row: `stubKept` is
cleared by the read that finds it filled, so the row stops saying items still
need it while showing a green check beside "You're all set".
A kept-stub row's button also goes straight to filling that placeholder rather
than reopening the chooser. A second retarget from there can only be a no-op —
every rewritable referrer is already off the stub — and it would have relabelled
the row after moving nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj
* fix: check staleness where it can be seen, and stop trusting a client-side licence
The post-rewrite check could no longer fail: since the rewrite became token-only
it ran over exactly what the check looked for, so it read as a guard while
guarding nothing. The staleness it named is real — the plan classifies items from
the search listings and each write re-reads its item by path — so the check now
happens on that fresh read, and looks for the spelling no rewrite reaches. A
referrer the plan already recorded as unreachable skips it: the stub survives
either way, and re-reporting the same item would say it was both unmovable and
changed underfoot.
Trigger kinds are no longer skipped by the client-side licence store. That store
is empty on an EE instance whose licence is unset or whose fetch failed, while
the rows are still in the database and the routes still answer — and a kind
skipped that way left no gap, so the stub went while an EE trigger still pointed
at it. On CE those routes are not registered and the 404 branch already says so,
from the server rather than from a store.
`askableResources` now pairs the export's resources with the retargeted ones by
position, the way `retargetProjectExport` maps them, instead of rebuilding the
path by slicing a prefix. An external path the bundle pulled in lands at
`f/<folder>/<name>` with a `_2` suffix on collision, which no slicing recovers —
and the row would have gone missing from a checklist the stepper still counted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj
* fix: a scan the caller is not shown all of cannot clear the stub for deletion
The listings the scan reads run as the caller, and row-level security filters
them inside the query. For anyone but a workspace admin that means an item they
cannot read is not absent from the answer so much as invisible in it: it does not
appear, and it does not count towards the full-page test that catches a truncated
listing either. A colleague's private script referencing the stub is exactly that
shape, so the scan reported a clean sweep and the stub was deleted out from under
it, with nothing said.
That is the one input to the completeness proof the destructive step rests on
that was never checked. A caller who is not shown the whole workspace now records
a gap like any other, so the rewrite still happens in full and the placeholder
stays.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj
* fix: ask whether this workspace's listings are complete, not a stale record's
`UserExt` is per-workspace and outlives a workspace change, which is why it
carries `workspace_id`. Reading `is_admin` off it without checking which
workspace it describes answers for the wrong one. Step 4 is reachable by reload —
it is built to be — and nothing on that path re-fetches the record, so it still
describes the workspace the user came from. An admin of their own workspace
importing into a shared one they are a plain member of got a clean scan over
row-level-security-filtered listings, and the stub was deleted under a referrer
they were never shown.
The question is now asked of the target workspace, through a predicate that can
be tested. An instance superadmin bypasses the policies everywhere, so that is
asked separately rather than read off the same stale record.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj
* style: format the wizard retarget files
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj
* fix: leave a trigger's runnable references alone, and read the app kind rather than guess it
A trigger's `on_failure`, `on_recovery`, `on_success` and `url` name a runnable,
and `rewriteTriggerConfig` remaps one on an exact match — right for the
folder-wide map the import hands it, wrong for a map holding a single resource
path. A schedule whose error handler ran a script sharing that path had the
handler pointed at the credential instead. The same reason `path` and
`script_path` were already restored; only the two prefixed shapes it remaps are,
so a field holding a `$res:` token still moves.
The scan guessed raw from low-code by looking for `files` and `runnables`,
because `list_search_apps` returns only the path and the value. Both writers
re-read the app anyway, and that record carries `raw_app`, so the write now
dispatches on it. A guess wrong in either direction was a deploy the backend
refuses for changing an app's kind, which aborted the run at that referrer.
Also drops the past-tense clauses from four test comments. Each already states
the invariant it guards; the rest described iterations of this branch that no
reader will have seen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj
* fix: restore a trigger's bare runnable references too
The prefixed spellings were put back after the rewrite; the bare ones were not.
`dynamic_skip`, `error_handler_path` and a websocket initial message's
`runnable_result.path` each hold a plain script path, which `rewriteTriggerConfig`
remaps on a whole-string match — so a trigger whose error handler ran a script
sharing the stub's path had that handler pointed at the credential.
All of them now come back from the row, taken from what `triggerHandlerRefs`
reads rather than enumerated by hand. A prefixed field is still restored only
when it holds the runnable spelling, so a `$res:` token in one still moves; a
bare field is a path and nothing else, so it is always restored.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: let operators use wmill.datatable() from within running jobs
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHR4fytgt6m4q37WCXs2Rp
* fix: refuse content-driven redirects and deferral in the operator datatable exemption
* fix: check the datatable exemption against the expanded query, not the raw content
* fix: fail closed on a language-overriding expansion and state the exemption's real scope
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
A workspace path mentioned in a chat message rendered as a link that always
opened a new browser tab. On the sessions page, which hosts a preview panel,
a plain click now opens the item in that panel instead. Modifier clicks still
reach a new tab, and surfaces with no panel keep their previous behaviour.
Scripts, flows and raw apps are supported. Legacy drag-and-drop apps are not:
the panel has no editor that can host one, so their links stay outbound.
The link pill's kind icon and action icon now cross-fade inside a fixed 12px
box, so the pill is the same width at rest and on hover and the surrounding
sentence never reflows.
`openItemPreviewAction` moves to a new import-free leaf module so a chat
message can reach it at runtime without dragging monaco, zod and the openai
client into the render path.
Claude-Session: https://claude.ai/code/session_01RjbVL7h9NiTLGTgyfiHvXG
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: record supplied script lock hashes so importers can skip relocking
Creating a script with a caller-supplied lock — a CLI push, a git-sync deploy,
any create carrying a lockfile — stored the lock on `script` but never wrote the
matching `lock_hash(workspace_id, path, hash_script(lock))` row. Only
worker-generated locks did.
`try_skip_relock` treats a missing hash for an imported script as changed, so no
importer of such a script could ever satisfy the skip predicate: every deploy of
it relocked every importer, forever.
The create transaction now records the hash for any lock it accepts, including
the empty one a codebase or a language with no lock generation carries — the
worker writes `hash_script("")` there, and a path going from a real lock to an
empty one has to stop matching what its importers recorded. Only a lock left to
a dependency job is skipped, because that job writes it.
A workspace clone now carries `lock_hash` too, without which every
dependency-map snapshot the clone later recorded held NULL and nothing in it
could ever skip. `dependency_map.imported_lockfile_hash` is deliberately not
copied: it records what an importer resolved against when it was last locked,
the clone runs READ COMMITTED, and a relock landing in the source between the
scripts being cloned and that statement would attach a hash the cloned
importer's lock was never resolved against — a hash older than the cloned
scripts costs one relock, a newer one skips a relock that was needed.
Lock generation is untouched, as is everything a relock does once it runs. The
only behavior that moves is which relocks are skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138oct9a6SLEZvFyCQgHRBx
* fix: narrow to the create-path lock hash
Drop the workspace-clone copy of lock_hash. It sits outside the reported
bug, and its double join over `script` can emit a path twice where two
versions are live, which the unique key on (workspace_id, path) then
rejects, failing the whole fork.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138oct9a6SLEZvFyCQgHRBx
* fix: restore the workspace-clone lock hash copy, guarded against fanout
A path can hold two live versions, and both joins match on path alone, so
the select can emit it four times against a primary key that admits one.
Every such row carries the single hash the path has, so ON CONFLICT DO
NOTHING settles it rather than aborting the fork.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138oct9a6SLEZvFyCQgHRBx
* fix: hash a clone's own locks rather than copying the source's rows
A source row is only as current as the last write to it, and a supplied
lock deployed before this was recorded leaves one naming a lock the path
no longer holds. Copying that into a fork hands an importer a hash it
never resolved against; hashing what the clone holds cannot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138oct9a6SLEZvFyCQgHRBx
* test: pin the lock hash written on a no-op push
Removing that write leaves the assertion with no row, which is the state
a script deployed before this shipped would stay in.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138oct9a6SLEZvFyCQgHRBx
* refactor: share one lock hash writer between the create and clone paths
Both wrote the same upsert with different SQL. The existing writers fold
theirs into the statement that writes the lock itself, which is what keeps
the two consistent; these two have nothing to fold it into, so they take a
shared one instead. The clone walks its pages by path rather than listing
them first, dropping a query with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138oct9a6SLEZvFyCQgHRBx
* fix: stream a clone's locks rather than reading them in pages
script.lock is unbounded, so a page of them is bounded only by how many
it holds. Hashing each as it arrives keeps one in memory at a time and
lets the clone site collapse to a single call.
Also states on both writers that they check no access to the workspace
they write, which their callers are the ones to have established.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138oct9a6SLEZvFyCQgHRBx
* fix: make the lock hash writer safe to repeat and free when unchanged
A path given twice in one call would have Postgres reject the whole
statement, so the last hash for each wins. And recording a hash a path
already has cut a row version for nothing on every unchanged sync, which
is the mode the no-op push runs in.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138oct9a6SLEZvFyCQgHRBx
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix: connect to dev server instead of localhost
* fix: derive WebSocket scheme from location.protocol
Mirror the protocol-aware pattern used by initSqlWebSocket in dev.ts
so the WebSocket connects over wss:// when the dev server is reached
through an HTTPS proxy/tunnel, avoiding mixed-content blocking.
* refactor: drop now-unused port parameter of wmillTsDev
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HsfdN82yP88qyQ3h8Lwv2v
---------
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: tolerate string app_id in GHES app config deserialization
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Rh73nHumzCbyd4Gwf6kw6
* fix: address review — strict app_id validation, drop dead variant
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Rh73nHumzCbyd4Gwf6kw6
* chore: update ee-repo-ref to b52c6471d517d979a9887f207a36347b1af376c8
This commit updates the EE repository reference after PR #767 was merged in windmill-ee-private.
Previous ee-repo-ref: ab2dc653719f9d65eb10964d1e2b5bc1b94d6535
New ee-repo-ref: b52c6471d517d979a9887f207a36347b1af376c8
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: portal the confirmation modal so drawers cannot cover it
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* fix: log a folder acl grant under the permission it granted
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* fix: keep a table's actions column at its right edge
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* feat: edit a folder in a drawer that saves once
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* refactor: call the people on a folder or item members
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* fix: edit a folder against the workspace the drawer targets
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* refactor: drop the now-unused sticky actions column
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* docs: correct the script editor drawer's modal placement note
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* fix: pin the actions column without losing the row's hover tint
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* feat: show the pinned column's seam only while the table overflows
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* fix: draw the pinned column's seam as a shadow so it does not scroll away
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* fix: fade the pinned column's tint in step with its row
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* refactor: address review nits on the folder editor and pinned cell
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* fix: keep the folder draft across a user-store refresh
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* refactor: extract and test the folder draft's dirty check and permission diff
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* fix: stop the folder editor showing state the server refused
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* fix: keep a folder draft that no request ever reached the server
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* fix: keep unapplied folder edits dirty when a save partially fails
* fix: block folder form edits while a save is in flight
* fix: commit a typed folder label before save snapshots the draft
* fix: count a typed folder label as an unsaved change
* fix: keep escape in the label input from closing what encloses it
* fix: capitalize folder table headers and drop a dead portal target
* refactor: make the confirmation modal portal opt-in per call site
* docs: name the stacking context that actually traps the discard dialog
* fix: report a half-landed member removal so the baseline reconciles
* feat: edit a group in a drawer that saves once
* fix: freeze the group name once the group exists
* fix: revoke the caller's own group acl last so the rest of the save is authorized
* docs: state the group call-ordering invariant once
* fix: report a failing post-save reload instead of dropping the rejection
* fix: hand the folder list reload back so a failure is reported
* fix: treat a rejected group create as inconclusive and catch a throwing onSaved
* revert: stop inferring a group was created from its name being taken
* fix: say when a failed group create may have saved the group anyway
* fix: key the may-have-been-created hint on the name conflict, not the status
* fix: skip the may-have-been-created hint when the group is known to exist
* feat: open a folder's group member from its row
* fix: stop showing the caller as an admin when the read failed
* fix: give up the caller's own folder admin last, and label a create as one
* fix: drop a folder member's acl before its owner entry
* fix: remove a folder owner before their acl, and correct the rls rationale
* docs: say the refusal is on the caller's last admin handle
* fix: defer only the folder rows the caller is an admin through
* docs: describe callerOwners as what the caller passes in
* docs: drop the call-site restatement of the diff's own invariant
* docs: record manager as a legacy group role
* fix: treat a sent request as possibly committed when reconciling
* fix: reconcile on any failed edit, and compare members as a set
* fix: keep write access when only the reconcile read fails
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(ai-chat): make reusable skills ai_skill resources you select per workspace
* chore: pin the ee ref to the skill telemetry counters
* fix: address review findings on skill authoring, import and migration
* fix: enforce skill selection in read_skill and stop imports clobbering resources
* feat: carry format_extension from the hub into synced resource types
* fix: let an edit set or clear a resource type's format_extension
* fix: regenerate the sqlx cache and close the review round findings
* fix: close the round-2 findings on folder ACLs, cached sync and truncation
* refactor: make the skills migration non-destructive and use design-system inputs
* fix: close the round-4 findings on folder owners, startup sync and truncation
* fix: clear obsolete extensions, guard folder owners, and report skipped skills
* fix: honor explicit-null extensions and report same-type migration conflicts
* fix: scope skill actions to the committed workspace and paginate the listing
* fix: keep the drawer scoped to the live workspace and surface truncation
* fix: discard a skills refresh for a workspace the chat has left
* chore: update ee-repo-ref to 6efe7a73c745c2e1377a34498523c00d89010a3d
This commit updates the EE repository reference after PR #764 was merged in windmill-ee-private.
Previous ee-repo-ref: 55998c142bc72edd08532748af1974b16035658d
New ee-repo-ref: 6efe7a73c745c2e1377a34498523c00d89010a3d
Automated by sync-ee-ref workflow.
---------
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
An `@`-mentioned workspace item stayed in `selectedContext` after the message
that mentioned it was sent, so every later turn in the session restamped it
into `## SELECTED CONTEXT`.
Treat those mentions the way a DOM pick is treated: attached to the one
message that carried them. The composer pins the live selection as
`contextOverride` at the click and clears the mentions in the same
synchronous gesture, so the send keeps what the user picked for it and the
next draft starts clean. When a send hands its text back to the composer,
the mentions it carried come back with it.
Scoped to GLOBAL. In SCRIPT/FLOW/APP the mentions still stay selected as
chips the user removes by hand, so `isMentionContext` is membership only
and every caller gates on mode.
Claude-Session: https://claude.ai/code/session_01HxGz1YsvW5Kwmn8THAUrwB
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: keep a local dbt descriptor under sync pull --keep-deleted
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174o6mGTWoanipUgf5zNcVL
* fix: keep an added-shaped dbt descriptor removal under --keep-deleted too
A stateful pull compares `.wmill`, not the working tree, so a descriptor
missing from that map still arrives as `added` while a real file with the
project's warehouse and run arguments sits on disk. Counting only `edited`
left that file deletable, and silently: the flag logged nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174o6mGTWoanipUgf5zNcVL
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: add free Claude Opus tier with per-user token limit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* nit move alert
* Home AI Chat
* wire home ai chat
* auto send prompt
* refactor: remove keyboard arrow-navigation from home list
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: replace home search bar with unified FilterSearchbar
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: replace home quick tags with FilterSearchbar presets
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: add content filter to home FilterSearchbar with EE-gated content view
- Clear the kind filter by deleting the key (was showing a 'kind: null' tag on All)
- Remove the standalone Content button
- Add a 'content' filter; when set, render the Ctrl-K content-search view
(ContentSearchInner) which shows text-match snippets and its own EE warning
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: disable home AI chat and prompt to configure AI when no model
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* track cost instead of tokens
* nit
* fix: load copilot config on home so AI chat isn't wrongly gated
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Home page update
* nits
* example prompts
* nit
* feat: switch free AI tier to DeepSeek with daily cost budgets
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* nit
* Move bottom buttons to HomeAIChat
* [ee] feat: surface free AI tier state and make its metering abort-proof
Makes the free Windmill AI tier legible to the user and closes an abuse hole.
Backend:
- AIConfig gains a response-only free_tier marker (skip_deserializing so a
client can't store a forged one via edit_copilot_config). get_copilot_info
keeps returning it once the grant is spent, so the client knows AI is off
because the grant ran out, not because nothing was configured.
- Per-user grant becomes one-time (migration drops the day key from
ai_free_token_usage); the daily table stays as the instance kill-switch.
- Reserve-then-reconcile metering (see EE commit) so a mid-stream disconnect
can no longer dodge the usage report and get metered zero.
Frontend:
- copilotInfo carries freeTier; model settings show a "Free" pill and a
usage meter that warns past 80%.
- The home chat and the session chat show a dedicated "you've used your free
Windmill AI, add your own API key" state instead of the generic
"no provider configured" one.
- A failed send re-fetches copilot_info so the exhausted state (and its
banner) appears live, without a page reload.
Bumps ee-repo-ref.txt to the matching EE commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: free AI usage meter reusing the context-usage gauge
Show free-tier spend with the same gauge as context usage instead of a
bespoke block:
- Extract the meter+tooltip into a shared UsageMeter; ContextUsageIndicator
uses it, and a new FreeTierUsageIndicator renders it from
copilotInfo.freeTier. Placed in the session-chat toolbar and next to the
home-chat model settings; the old meter block in the model-settings
dropdown is removed (the "Free" pill stays).
- Hide the context-usage bar while on the free tier so the free meter takes
that slot.
- Refresh copilotInfo after every free-tier turn (AIChatManager finally) so
the meter advances live and the turn that exhausts the grant flips to the
exhausted state, instead of both only updating on reload. Gated to active
free-tier users, so it costs nothing for configured-key users.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: fix stale free-tier comments after DeepSeek/cost rework
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: always show context bar, replace free-tier meter with usage banner
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* nit
* fix: atomic free-tier budget reservation (ee ref + sqlx)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: keep CLI/MCP and Hub buttons unblurred on AI chat hover
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add back arrow nav
* nit
* nit
* fix: three review P1s in the home AI chat & search
- AIChatManager: refreshFreeTierUsage now bails unless the global copilot
state still belongs to the completing manager's workspace, so a warm
session finishing after a workspace switch can't reload its (background)
workspace over the active one's models/client/copilotWorkspace.
- HomeAIChat: block submission until the copilot config is loaded AND
enabled (new `canSend`), so a prompt submitted during the unknown-config
window isn't handed to a session that never sends it and silently lost.
The disabled overlay still gates on config-loaded to avoid a flash.
- ItemsList: the content-search reload effect now depends on $workspaceStore
so content results follow the active workspace instead of showing the
previous one's.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* [ee] fix: harden the three home-AI-chat/search P1s after deeper review
Follow-up to the previous P1 commit; sharper review found the earlier guards
insufficient:
- refreshFreeTierUsage now compares against the most-recently-*requested*
workspace (new copilotWorkspaceRequested in aiStore, set synchronously in
loadCopilot), not the last-*resolved* one — otherwise a warm session
finishing while a newer workspace's load is still in flight could win the
monotonic token and restore its stale workspace over the one being loaded.
- The content-search view is keyed by workspace ({#key $workspaceStore}) so a
switch remounts ContentSearchInner; late in-flight responses from the
previous workspace can no longer land in the new one's component.
Backend (EE, via ee-repo-ref bump to 03ef0eb): the free-tier reservation now
also prices the worst-case input cap (at the cache-miss rate), and
enforce_free_tier_body rejects oversized prompts and pins n=1 — so an aborted
large-prompt request can no longer dodge the input bill that reconciliation
would otherwise charge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: exclude service accounts from the free AI tier
Free-tier eligibility was keyed solely on authed.email. Workspace admins can
create and impersonate arbitrary service accounts (synthetic *.sa.wm.dev
identities), each of which would receive its own one-time grant — letting one
tenant mint many grants and drain the instance-wide daily allowance. Skip the
free-tier fallback for *.sa.wm.dev identities.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: activate free AI tier when clearing a workspace provider
edit_copilot_config returned AIConfig::default() when the saved workspace
config had no providers and no instance config existed; the frontend applies
that response immediately, disabling AI even though the free-tier key is
available. A later get_copilot_info (on reload) returns the synthetic free-tier
config, so clearing a provider behaved inconsistently until reload. Give this
response path the same free-tier fallback as get_copilot_info.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: gate the home AI composer behind the global-AI dev flag
The "Build with AI" composer starts a session and navigates to /sessions, which
lives behind the same wm_dev_global_ai dev gate as the global AI chat. With the
gate off (the default), /sessions renders only its gate message, SessionWrapper
never mounts, and the queued prompt is silently dropped. Hide the home entry
point behind isGlobalAiEnabled() so it isn't exposed before the sessions gate
opens.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* [ee] chore: bump ee-repo-ref for deepseek-v4-flash price/model fix
Points at the EE commit that pins deepseek-v4-flash and its real prices
(pico-precision accounting).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* [ee] fix: provable byte bound for the free-tier input cap (ee-repo-ref)
Bumps ee-repo-ref to the EE commit that caps the raw request body byte length
directly (token_count <= byte_count is provable), replacing the unsafe
body.len()/2 token estimate that high-entropy prompts could beat.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* nit isGlobalAiEnabled
* empty commit
* fix(frontend): address Codex review on free-tier / home filters
- P1: home filters now sync from the URL reactively, so browser Back/Forward
updates the chips, kind toggle and results (and clears keys dropped from the
URL) instead of leaving them stale until the next filter edit.
- Free-tier banner buttons drop deprecated Button props (size/color/border
variant) for unifiedSize + a supported variant.
- Condense refreshFreeTierUsage comments to a single race-condition constraint
beside the guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(frontend): hide empty kind badge on draft-only scripts
A draft-only script can carry an empty `kind`, which still isn't 'script' so the
row rendered a blue badge whose only content was capitalize('') — an empty pill
left of the "Draft only" badge. Guard the badge on a non-empty kind.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(frontend): animate home tree-view group expand/collapse
Wrap each owner group's children in ResizeTransitionWrapper so height changes
animate. A slide transition only animates the initial mount, but a freshly-opened
owner fetches its rows and passes through a transient empty state before they land
— the ResizeObserver animates that second growth too. Nested TreeViews inherit the
wrapper's context and skip their own, so one observer per top-level owner animates
the whole subtree.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(frontend): FilterSearchbar boolean auto-set and string-filter presets
- A default-false boolean filter has only one useful value, so selecting it sets
true immediately instead of opening a true/false picker. A default-true boolean
(e.g. "Include library scripts") still shows the picker, where false is the
meaningful choice — expressed via a new optional `default` on the schema.
- A plain string filter now surfaces any presets targeting it (`<tag>:<value>`)
as suggestions once selected, integrated into menuItems so keyboard nav works —
previously selecting e.g. "Owner" showed nothing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(frontend): home page toolbar and content-filter revamp
- "New" create-menu button (scripts/flows/apps/…) replaces the old Content button;
the search bar moves to the right of the toggle group.
- Restore the content filter dropped in a merge: a `content` searchbar filter swaps
the list for the full-text ContentSearchInner view (EE), aligned flush with -mx-2.
- Move the owner/group and label chips off the page into FilterSearchbar presets;
ownerFilter/labelFilter now derive from the searchbar keys (data layer unchanged).
- Move the list controls (select / tree view / expand-all / sort) inline into the
top row between the toggle group and search bar; add margin above the list.
- Beta tag on the home AI chat; a bit more bottom margin under it; tighten the gap
between the admin/tutorial banners and the list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai): pass the request body to the free-tier reservation
Thread the prompt body into resolve_free_tier_credentials so the free tier can size its
upfront reservation from the actual request length instead of a fixed worst case (EE
c2e248b), fixing normal chats being rejected as "too large". Updates the OSS stub signature
and bumps ee-repo-ref.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(frontend): gate home Create/Import menu on edit permissions
The relocated CreateActionsMenu rendered unconditionally, so operators and users in
workspaces protected from direct deployment saw create/import actions they can't use.
Restore the original gate (!operator && showEditButtons, the latter from NoDirectDeployAlert).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(frontend): address Codex review on filter searchbar
- P1: the boolean shortcut now goes through the same tag-insertion path as the normal
branch, so it removes the typed search segment instead of leaving it as a stray
free-text (_default_) term.
- Mark the Runs `show_future_jobs` filter default: true so selecting it opens the picker
(false is the meaningful choice) rather than being a no-op.
- Home owner/label presets now emit the canonical `key:\ value` form so the applied-preset
check matches after a reparse and can't re-offer a duplicate; update the suggestion
extraction to strip the leading separator.
- Replace deprecated Button props (size/spacingSize/color) on the relocated list controls
with unifiedSize.
- Fix stale comments: UsageMeter no longer claims a free-tier consumer; the home filter
schema comment describes presets, not the removed ListFilters/label badges.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(frontend): boolean filter shortcut sets value canonically
The round-1 shortcut baked `true` into the tag text, which merged into a following tag
(e.g. `archived:\ truekind:\ flow`). Instead remove the typed segment, set the value, and
reparse so the text is rebuilt canonically — no lingering free-text and no merge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(ai): restate free-tier caller identity contract in the OSS stub; bump ee-repo-ref
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(frontend): keep flanking tags separate when boolean shortcut drops a segment
Joining `before`/`after` directly fused the tags a removed mid-segment sat between
(e.g. `kind:\ flowsummary:\ bar`). Join with a space; reparse then canonicalizes. Also
trims the comment to the essential constraint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(ai): update sqlx cache for free-tier daily-day queries; bump ee-repo-ref
The reserve/reconcile daily-usage queries now bind the reservation day (EE change); refresh
their offline query cache and point ee-repo-ref at the EE commit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai): activate free tier when instance ai_config has no provider
An instance ai_config row won precedence just by existing, so an empty {} (valid via global
settings / declarative config) suppressed the free-tier fallback and left AI disabled — even
though build_copilot_settings_state already treats it as unconfigured. Apply the same
has_providers() check to the instance config in the proxy and edit_copilot_config paths.
Also refresh the sqlx cache for the reservation ceiling change and bump ee-repo-ref.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(frontend): migrate legacy Home filter URLs to the searchbar keys
The old Home UI stored free-text in `search`, owner scope in `filter`, and could write
`kind=all`; the generic searchbar sync uses `_default_`, `owner`, and a kind enum without
`all`. Rewrite those params once before the sync reads the URL so shared/bookmarked links
restore, and drop `kind=all` which would otherwise wedge later filter edits.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ai): empty instance config in get_copilot_info; label user-disabled Home AI
- get_copilot_info returned any existing instance ai_config row before the free-tier
fallback, so an empty {} disabled AI in the copilot-info UI even though the proxy now
serves the free tier. Apply the same has_providers() gate here.
- The Home chat overlay said "No AI provider is configured" when the user had disabled AI
in account settings (providers still present). Distinguish that state ("Windmill AI is
disabled in your account settings") as the docked chat does, and drop the misleading
workspace-config button in that case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(ai): drop redundant proxy service-account check; trim TreeView comment
The service-account exclusion now lives in the free-tier helper, so the proxy calls it
directly. Also condense the tree-view resize-transition comment to the essential reason.
Bumps ee-repo-ref.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(frontend): the Home content filter is not EE-gated
ContentSearchInner loads the workspace's scripts/flows/apps/resources and matches their
contents client-side, so it works on any instance. Drop the misleading "(EE)" from the
filter label and the "EE indexer / off-EE fallback" comments.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(ee): bump ee-repo-ref for free-tier pricing + exhaustion fixes
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSqa1iRxn9GUE9fegT7bDS
* fix(frontend): show disabled Home AI overlay statically, not on hover
The disabled-state overlay (reason + configure/add-key action) was opacity-0 and
pointer-events-none until group-hover, so keyboard and touch users saw an inert composer
with no visible remedy. Render it and the composer blur statically when disabled instead.
Also bumps ee-repo-ref for the trimmed free-tier comments.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSqa1iRxn9GUE9fegT7bDS
* fix(frontend): give account-disabled Home AI overlay a recovery action
The account-disabled branch showed a reason but hid every action, on the mistaken premise
that account settings has no linkable route. It opens from the #user-settings hash (the
same one the sidebar Account menu uses), so link there. Bumps ee-repo-ref for the
free-tier fixes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSqa1iRxn9GUE9fegT7bDS
* fix(frontend): gate Home AI composer for operators; a11y and filter-sync fixes
- Home composer now uses prefersSessionHandoff($userStore?.operator) instead of
isGlobalAiEnabled(): operators reached this route and could submit a prompt into a
/sessions page that refuses them, silently dropping it. Also drops the leftover empty
header spacer div above the chat.
- HomeAIChat: mark the blurred/disabled subtrees inert so keyboard users can't tab into
the unreadable textarea (pointer-events-none didn't stop Tab).
- ItemsList: keep the role-dependent searchbar keys (include_library, only_user_folders)
in the schema unconditionally and toggle `hidden` instead, so useUrlSyncedFilterInstance
(which snapshots the key set once) still URL-syncs a key that first appears after a
workspace switch.
- Bumps ee-repo-ref for the indexer non-parquet build fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSqa1iRxn9GUE9fegT7bDS
* fix(frontend): keep CLI/MCP connect row for operators; trim filter comment
The previous commit gated all of HomeAIChat behind the operator/session check, which also
removed the AI-independent CLI/MCP "Connect workspace" drawer that operators (and the
sessions-beta opt-out) had on main. Render HomeAIChat for the same audience as before
(isGlobalAiEnabled) and gate only the composer (title, input, examples, overlay) on
operator status inside the component; the connect row always shows. Also trims the
role-dependent filter-schema comment to the <=4 line rule.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSqa1iRxn9GUE9fegT7bDS
* fix(frontend): reconnect Home keyboard navigation to the unified searchbar
The searchbar migration replaced the <input id="home-search-input"> the ItemsList keyboard
handler keys off, so Arrow/Enter no longer drove the results list. Thread an `id` down to the
searchbar's contenteditable (via TaggedTextInput/FilterSearchbar `inputId`) so the handler and
the workspace-switch focus restoration find it again; read the caret through the Selection API
instead of an <input>'s selectionStart/End; and stand the list's arrows down while the
searchbar's suggestion dropdown is open (tracked via onDropdownVisibleChange). In free-text
mode the searchbar no longer opens its dropdown on a bare arrow key, so an empty box passes
Arrow/Enter to the list as before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSqa1iRxn9GUE9fegT7bDS
* fix(frontend): stop searchbar Enter inserting a newline; idle typewriter for operators
- TaggedTextInput is a single-line filter input, so Enter now preventDefaults the
contenteditable's newline insertion (surrounding suggestion-select / list-open handlers
still run on bubble). Previously Enter with no row highlighted dropped a literal \n into
the query.
- HomeAIChat's placeholder typewriter effect now runs only while the composer is shown, so
it no longer loops forever driving an unrendered input for operators.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSqa1iRxn9GUE9fegT7bDS
* chore: update ee-repo-ref to f2a31156ac08ecb02d89dbc66d72be58e9c877ff
This commit updates the EE repository reference after PR #652 was merged in windmill-ee-private.
Previous ee-repo-ref: e59b96a2eea5d1110b40c842f17b337ab051bdd3
New ee-repo-ref: f2a31156ac08ecb02d89dbc66d72be58e9c877ff
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: add service log documents to the index one batch at a time
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014KnE8my2okxQGCx47cWMjf
* chore: update ee-repo-ref to df60763d1f243b0048dfc3fe700bc026b257bea8
This commit updates the EE repository reference after PR #762 was merged in windmill-ee-private.
Previous ee-repo-ref: f9a0b98080eecdc2885720e0f8506933a0675bb5
New ee-repo-ref: df60763d1f243b0048dfc3fe700bc026b257bea8
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* [ee] fix: an unreadable ingest cursor should not stop the server booting
Three follow-ups to #10894, all in the service log indexer: a corrupt cursor no
longer takes the server down at boot, the queue's writes are covered against a
real database rather than by hand, and a read skips the dedupe when the partition
it reads holds a single object.
* [ee] test: place the queue's rows relative to the clock the statement reads
Also drops the two `.sqlx` entries the query extraction orphaned: sqlx keys on the
literal including its indentation, so moving a query into a function leaves the
old copy behind.
* [ee] test: make the pair-exactness and rebuild-dedupe tests actually bite
* [ee] docs: state the cursor and dedupe rules without their history
* chore: update ee-repo-ref to 90a368362896ebcc2fcfaaf9510dc9be68c929f7
This commit updates the EE repository reference after PR #761 was merged in windmill-ee-private.
Previous ee-repo-ref: e3423705aa8f2d585bc65474cfd0c4c762ec4ad5
New ee-repo-ref: 90a368362896ebcc2fcfaaf9510dc9be68c929f7
Automated by sync-ee-ref workflow.
---------
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix: track outstanding service log files on the rows themselves
Adds `log_file.indexed_at` so the service log ingest can read outstanding rows
instead of walking a cursor over `log_ts`. A row registered after the pass had
gone by its minute was skipped for good, and no ordering fixes that — an arrival
sequence fails the same way, since a row can take a lower value and commit after
a higher one has moved the cursor past it.
The migration marks existing rows with a sentinel; the first pass returns the
ones the old cursor had not reached to the queue.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EPAP96jJNYpPQ8bpxZcU1C
* [ee] refactor: drop the claim/confirm phase from the service log ingest queue
Two states are enough: a row is outstanding or it is marked. The migration no
longer creates the index for the claim sentinel, and the sqlx cache loses the
two queries the event-time cursor used.
* [ee] fix: make re-indexing a service log file idempotent
Corrects the `init_last_log_file_sent` note: a rewritten row keeps the
`indexed_at` it had, so one the indexers already took is not offered again.
* [ee] fix: let a rebuild take the rows it covered out of the ingest queue
Adds the query that releases them; the index layout stays v4.
* [ee] fix: index the lookup a rebuild releases rows by
A rebuild takes rows out of the queue by the file it read out of the store, which
is the one lookup that arrives without a `log_ts`. The primary key is
`(hostname, log_ts)`, so nothing covered it and each batch scanned every
outstanding row — worst in exactly the state a rebuild follows. Verified at 50k
outstanding rows: sequential scan becomes an index scan.
Also records `log_file.indexed_at` in the schema reference.
* [ee] fix: treat a state handed back without its line count as behind
* [ee] fix: give the converted state a line count
* [ee] fix: keep the converted cursor from being rewound by the rebuild
* [ee] fix: inherit the legacy cursor from one source, not field by field
* [ee] fix: count a file's lines against the buffer before reading it
* [ee] fix: bound the row buffer on what it holds, not on reported counts
* [ee] fix: settle the upgrade from the store rather than from event time
* [ee] docs: describe the conversion's second half as it now works
* [ee] refactor: settle the upgrade with one rebuild instead of reconciling
The migration records existing rows as done rather than marking them with a
sentinel: the indexer puts back what the old cursor had not reached on its first
pass, which is the only place that cursor's position is known.
* [ee] fix: repair the rows the old cursor skipped instead of recording them as done
The migration marks pre-existing rows with a sentinel again, so the indexer can
tell them from rows registered since and put the window's worth back on the queue.
* [ee] fix: keep a source file whole in one partition
* [ee] revert the file-atomic partition change
* [ee] fix: dedupe the public reads, and repair an index without a cursor
* [ee] fix: repair an index whose cursor is gone, and keep what the repair found
* [ee] fix: seed a pass from both axes of what a rebuild recovered
* [ee] fix: settle the cursor on what the store holds, not on what was read
* [ee] fix: an empty rebuild must not claim ground it has not covered
* [ee] test: pin the cursor a rebuild settles on
* chore: update ee-repo-ref to bc0c7051585194474078b6c1941a3fb73893d9e5
This commit updates the EE repository reference after PR #755 was merged in windmill-ee-private.
Previous ee-repo-ref: 328f5a90afeae9c683bf3294f0d9eb293a3e1a92
New ee-repo-ref: bc0c7051585194474078b6c1941a3fb73893d9e5
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix: upgrade argon2 to 0.6 and migrate the password hashing API
* test: pin that an unparseable stored hash reads as a failed login
* chore: update ee-repo-ref to 58738c39ac41d57917bbd9400318704763d997f7
This commit updates the EE repository reference after PR #759 was merged in windmill-ee-private.
Previous ee-repo-ref: 02a89fc4d27e49a494112fa91a8812e3ee4fb8a6
New ee-repo-ref: 58738c39ac41d57917bbd9400318704763d997f7
Automated by sync-ee-ref workflow.
---------
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat: day-partition the service log index and expire whole chunks
The service log index becomes one tantivy index per UTC day. The substance is
in windmill-ee-private#753; this side carries the EE ref and moves the log
indexer writer instead of cloning it, because sealing a chunk takes sole
ownership of its tantivy writer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EeUmYWCJeaaHutiHLfZBQX
* fix: do not adopt the superseded watermark after an explicit index clear
A clear asks for the retention window to be read again, and a watermark says
it already has been — and the v3 copy in object storage is kept for rollback,
so it outlives the local one the clear removes. Both copies of that watermark
are now read and the newer wins, for the same reason the v4 one is taken from
the store when it is ahead: a replica that lost the lock keeps a local file
frozen where it stopped while the store went on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EeUmYWCJeaaHutiHLfZBQX
* fix: delete a day's raw files at its checkpoint, and rebuild whole days
Bumps the EE ref for windmill-ee-private#753.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EeUmYWCJeaaHutiHLfZBQX
* fix: make an interrupted rebuild detectable, and pin the rebuild floor
Bumps the EE ref for windmill-ee-private#753.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EeUmYWCJeaaHutiHLfZBQX
* fix: keep the rebuild marker in the object store, not on local disk
Bumps the EE ref for windmill-ee-private#753.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EeUmYWCJeaaHutiHLfZBQX
* fix: two more routes to a partial index being accepted as complete
Bumps the EE ref for windmill-ee-private#753.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EeUmYWCJeaaHutiHLfZBQX
* fix: trust a local chunk only when the tracker vouches for it
Bumps the EE ref for windmill-ee-private#753.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EeUmYWCJeaaHutiHLfZBQX
* chore: condense the stale-chunk guard's doc to the four-line limit
Bumps the EE ref for windmill-ee-private#753.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EeUmYWCJeaaHutiHLfZBQX
* chore: update ee-repo-ref to 17ef439b087b400889ff19109be9d2c810142278
This commit updates the EE repository reference after PR #753 was merged in windmill-ee-private.
Previous ee-repo-ref: 3e79901b4742906d2285dd943e24fac0f735f199
New ee-repo-ref: 17ef439b087b400889ff19109be9d2c810142278
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
windmill-ee-private#756 was squash-merged, so the commit ee-repo-ref names is not on EE main
and the branch carrying it is gone. The content is identical, so nothing builds differently —
but a dangling ref is one garbage collection away from an EE build that cannot fetch what it
is pinned to.
Claude-Session: https://claude.ai/code/session_01WsnpNSM6K3oyjwntRwJtVN
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: make the service log retention period an instance setting
Service log retention was a hardcoded 14 days with no override, unlike job retention. It
becomes the `service_log_retention_secs` global setting (env `SERVICE_LOG_RETENTION_SECS`,
default unchanged at 14 days), reloaded on change like the other retention settings.
The constant becomes `DEFAULT_SERVICE_LOG_RETENTION_SECS` and every reader goes through
`service_log_retention_secs()`, so the `log_file` sweep, the object-storage orphan scan, the
columnar store's compaction and pruning, the retrieval clamp and the search index's trim
window all follow the configured value.
Loaded outside `initial_load`'s `server_mode` guard: a dedicated indexer trims the search
index to a window derived from this value and is not a server.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsnpNSM6K3oyjwntRwJtVN
* fix: never let a non-positive service log retention expire every log
Every service log cutoff is `now - retention`, so a `0` or negative window puts the cutoff
at or after `now` and the next sweep reads the whole history as expired — deleting the
`log_file` rows and their object-storage files irreversibly.
`0` is reachable two ways now that the window is configurable: it is what an operator types
by analogy with the job retention period sitting directly above it, where `0` does mean keep
forever; and `SecondsInput` writes a `0` into a field that was merely focused, so saving the
Jobs panel is enough. Service logs always have a window, so clamp an unusable value back to
the default in the accessor every reader already goes through. The upper bound is where
`chrono::Duration::seconds` panics, which would abort the sweep that reads it.
The settings field rejects a non-positive value rather than silently correcting it, and its
description now names the database rows too — they are swept on every instance, including
one with no object storage configured.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsnpNSM6K3oyjwntRwJtVN
* fix: address review findings on the service log retention setting
- Bound the monitor's `log_file` sweep. Every process rotates a log file a minute, so lowering
the retention can make one ordinary setting change expire millions of rows; the unbounded
`DELETE ... RETURNING` materialized all of them, and their deletion futures, in a single
tick. Batched like the settings-page cleanup on the same table.
- Make the retention atomic private and give it one writer, so a value that would expire every
service log cannot reach a cutoff by any path, and say so in the log when one is rejected
rather than falling back silently.
- Cap the retention at a century. The previous ceiling only bounded `TimeDelta` construction,
while consumers compute `now - retention`, which panics past year 262143, and build a
Postgres interval that overflows well before the old cap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsnpNSM6K3oyjwntRwJtVN
* fix: cap an oversized service log retention instead of shortening it
The two unusable directions were landing on the same fallback, so configuring a retention
above the ceiling silently produced 14 days — deleting logs the operator had asked to keep
for longer. Too large now caps at the maximum, which preserves that intent; only a
non-positive value, which would expire everything and has no upward reading, falls back to
the default.
Also bound the `log_file` drain to ten batches per pass: `monitor_db` runs under a 600s
timeout that cancels every maintenance future in the same `join!` and reports a critical
error, so a backlog large enough to need batching has to drain across ticks, the way the
neighbouring sweeps already do. The settings field carries the upper bound too, and the
superseded query's offline entry is dropped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsnpNSM6K3oyjwntRwJtVN
* fix: route the new log-file registration cutoff through the retention accessor
`send_log_files_to_object_store` arrived on main while this branch was open and reads the
retention directly. The atomic behind it is private now, so it goes through the accessor like
every other consumer — which also means the cutoff it uses to skip registering already-expired
files follows the configured retention rather than a fixed two weeks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsnpNSM6K3oyjwntRwJtVN
* fix: say why every mode loads the service log retention setting
A worker registers its rotated log files against the retention cutoff, so the comment naming
only the indexer no longer covers why the setting sits outside the `server_mode` guard.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsnpNSM6K3oyjwntRwJtVN
* fix: file service log retention under Monitoring, not Jobs
Service logs are the Windmill processes' own logs — every process rotates and registers its
own, no job involved — so the Jobs panel was grouping by the shape of the widget rather than
by the subject. It sits under Monitoring now, beside the Indexer panel that holds the other
service-log window.
Its own section rather than inside that panel: the panel is badged EE, while this governs the
database sweep that runs on every instance.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsnpNSM6K3oyjwntRwJtVN
* chore: update ee-repo-ref to a6e3533b26195918a17fea58646f71d2bbcde288
This commit updates the EE repository reference after PR #752 was merged in windmill-ee-private.
Previous ee-repo-ref: 1d93da24bd166b9a5a5cc204034a1d35ffc88474
New ee-repo-ref: a6e3533b26195918a17fea58646f71d2bbcde288
Automated by sync-ee-ref workflow.
* feat: say on the service logs page where the logs actually are
The retention number alone does not tell an operator what it governs, and the answer differs
by instance. Two states are worth calling out because they are the ones where retention does
not mean what it looks like:
Without instance object storage, each process keeps its files on its own disk. The page lists
what every host wrote, since the rows are in the shared database, but can only open the files
of the replica serving the request, and a host's files go with it when it is replaced.
With object storage but "Delete logs from s3 periodically" off — the backend default, since
uploads are gated on a store existing while deletions are gated on that toggle — expiring a
log removes the row and the local file and leaves the uploaded copy behind for good.
The retention field itself now names every copy it covers and says that full-text search
reaches back at most that far, and less when the indexer's own window is shorter.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsnpNSM6K3oyjwntRwJtVN
* fix: describe raw log files as the transient copy they became
Retiring the raw files landed while this was being written: the indexer now deletes each one
as soon as it is ingested, and the log viewer rebuilds a file from the columnar store once the
raw copy is gone. So the durable copy is the store, and warning that an uploaded file is kept
forever when periodic s3 deletion is off only holds where no indexer runs to ingest it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsnpNSM6K3oyjwntRwJtVN
* chore: point ee-repo-ref at the EE compile fix
EE main does not build on its own: extracting the index-window expression and adding a fourth
copy of it landed in separate PRs that never conflicted textually. windmill-ee-private#756 is
the one-line fix; this pins it so CI has a tree that compiles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsnpNSM6K3oyjwntRwJtVN
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat: serve service log context from the parquet store and retire the raw files
* fix: keep the log ingest cursor in the store and stream file rebuilds
* fix: roll back a partial index rebuild and move the cursor before the commit
* fix: make the index rebuild idempotent and repair a cursor the index never caught up with
* fix: seed the indexed cursor on upgrade and after a rebuild
* fix: fail the indexing pass on an unreadable cursor instead of reading it as absent
* docs: record what keeps both known_ts entries, not the path main removed
* chore: update ee-repo-ref to 466eb1830879052a5d042295256a78375bee916d
This commit updates the EE repository reference after PR #754 was merged in windmill-ee-private.
Previous ee-repo-ref: ddb3a536b8d85c134c01f87da7783baaa204a6d1
New ee-repo-ref: 466eb1830879052a5d042295256a78375bee916d
Automated by sync-ee-ref workflow.
---------
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix: unify billable seat counting and prevent fork subscriptions
* fix: authorize candidate before reading its plan, scope seat breakdown
* chore: pin ee ref for the stripe checkout fork guard
* fix: grant the billable_member view and widen the paid-plan check
* refactor: keep the seat rule in rust instead of a view and function
* docs: correct the attach guard summary after widening the plan check
* revert: keep cloud out of the ci test feature set
* chore: update ee-repo-ref to 9ff97cd818e85940fec282c92161e98c1b8583e2
This commit updates the EE repository reference after PR #742 was merged in windmill-ee-private.
Previous ee-repo-ref: 0ec0b42565a41f271a45bf24a93467d110c36df3
New ee-repo-ref: 9ff97cd818e85940fec282c92161e98c1b8583e2
Automated by sync-ee-ref workflow.
---------
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
* fix: strip the script/ prefix from trigger error handler paths
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: strip the script/ prefix when collecting trigger handler refs
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: relocate prefixed trigger error handlers on project retarget
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: reject a prefixed error_handler_path on triggers instead of resolving it
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: describe error_handler_path as a bare script path in the api schema
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix: key build artifact caches on a runnable's inline modules
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: seal the cache-key base and skip prebundling multi-file bun scripts
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: tighten cache-key invariant comments and name the retained-artifact residual
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: version the build artifact keyspace so pre-fix artifacts are abandoned
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: namespace the artifact cache by keyspace version instead of the hash preimage
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: namespace module-bearing artifacts instead of versioning the whole keyspace
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test: pin the cache-name base seal and name the retained-artifact residual
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: bump ee ref for agent-worker module resolution fix
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: align agent-worker module resolution with the worker for previews by hash
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: drop calculate_hash imports left unused by artifact_cache_name
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: update ee-repo-ref to 2d6c66b32f20d9605c6a677727473ab66fcc8a87
This commit updates the EE repository reference after PR #743 was merged in windmill-ee-private.
Previous ee-repo-ref: efce983cae3d53175bbb286a10205a2a360c2a9e
New ee-repo-ref: 2d6c66b32f20d9605c6a677727473ab66fcc8a87
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 <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>
* feat: track AI fill, AI fix, evals, reusable agents and debugger usage
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: pin ee ref to the feature_usage registry commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: update ee-repo-ref to c3b6f62ea579a3583d4b474e9885c77104cfc87e
This commit updates the EE repository reference after PR #745 was merged in windmill-ee-private.
Previous ee-repo-ref: 77992910929188a854eadc06ee45971877b6f954
New ee-repo-ref: c3b6f62ea579a3583d4b474e9885c77104cfc87e
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 <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): keep nested template literals intact in template inputs
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: fail a flow step with an unresolvable $args tag instead of hanging
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): surface input expression errors when running a step test
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): treat an escaped \${ as literal text when escaping backticks
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: accept the string "null" as a tag component, reject only JSON null
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: leave a same_worker step's inert tag alone, log an unresolved flow tag
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): escape every backtick when the template walk desynchronizes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: leave a dedicated runnable's inert step tag alone
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: reroute a step only when its own tag is what failed to resolve
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor: name the inert-tag guard step_is_pulled_by_tag
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: reject a tag only when it interpolates to nothing at all
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): validate the template walk instead of trusting a balanced stack
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: describe what an unresolvable tag actually interpolates to
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(frontend): decide template escaping with a real parser, not a hand-rolled scan
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor: name is_flow_step on push now that it is load-bearing
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): heal an expression escaped before nested templates were handled
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: reroute a step whose tag reads args that failed to evaluate
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: never hand a job that failed before running to a dedicated runner
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: reroute only a step whose args failed, leave other tags untouched
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: drop the post-preprocessor tag fallback, leaving tag resolution untouched
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor: leave interpolate_args exactly as it was
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test: use a generic example in the template literal tests
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): show an expression escaped by the old rule as it was authored
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): surface input expression errors from every step-run entry point
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: state what is_dedicated_worker actually reads
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): heal only text whose backticks were all escaped by the old rule
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): match the old rule textually so an authored backslash still heals
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(frontend): heal only expressions the old rule broke, never ones that parse
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* docs(api): document large completed job result placeholder
* style(api): use spaces for the large-result description indentation
Co-authored-by: Diego Imbert <70353967+diegoimbert@users.noreply.github.com>
---------
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Diego Imbert <70353967+diegoimbert@users.noreply.github.com>
* fix: pre-fill the test panel JSON args editor and align its placeholder
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: reseed the JSON args editor when the preprocessor tab is selected
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: seed schema defaults and own-property args in the JSON payload
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: follow the schema in an untouched JSON payload, ignore same-tab clicks
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: latch JSON editor ownership from Monaco, drop the late remounts
The pristine check read the bound `code` value, which trails the buffer by
SimpleEditor's 200ms debounce — a reseed arriving in that window overwrote text
already typed. Latch ownership from Monaco's own change event instead, via a new
undebounced `input` event guarded so `setCode`'s `setValue` does not read as an
edit.
Both `.then(() => argsRender++)` bumps are gone: the arg views now remount at the
tab transition only, and follow the schema in through `initialCode` when
inference resolves, so a remount can no longer land on an in-progress payload.
`FlowPreviewContent.selectInput` overwrote the editor on select but not on
deselect, leaving the abandoned input's payload over reverted args.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: let a job token read the automate_username_creation setting
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test: use an ungated global setting as the confinement control
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(frontend): guided setup wizard for data tables
On Cloud a data table cannot use the Windmill instance database, so a new
workspace hit a dead end: an alert telling the user to go find a PostgreSQL
resource somewhere else. Setting one up meant three disconnected places, and the
connection could only be tested after the config had already been saved.
Adds a three-step wizard (choose a database -> set it up -> name it) reached from
the data tables settings page:
- Supabase: signs in via the existing supabase_wizard OAuth client and creates
the project from inside Windmill. Because db_pass is an input to project
creation, Windmill sets the password and the user never visits a dashboard.
- Your own database: picks an existing postgresql resource, or adds one with a
connection string through the form that already supports it.
- Windmill database: hands back to the inline row editor, since instance
databases are provisioned by a superadmin.
Verifying access is no longer a step the user takes: Continue runs the check and
passing it is what advances the wizard, so a database that cannot create tables
never reaches the workspace config.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: pin ee-repo-ref to the Supabase provisioning endpoints
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): do not claim the database is ready when its check failed
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: address review findings on the data table wizard
- The Supabase create branch advanced on `provisioning === 4` without consulting
the check it had just run, so a role that cannot create tables could reach
Finish. It now blocks and offers Try again.
- Retrying no longer mints a fresh secret variable + resource each time: the
credentials are only re-created when the password actually changed.
- The generated password is captured before the create call rather than after,
since a throw there can still leave a project behind.
- On a failed provision the project list is refreshed, so the just-created
project can be picked up from the other tab instead of provisioning a second.
- Finish refuses a name that already belongs to another data table, which
previously repointed it at the new database.
- Secrets go to the acting user's namespace instead of a literal `u/admin/`.
- The progress list no longer ticks "Created on Supabase" before the request is
sent, and does not claim the database is ready when its check failed.
- The wizard's resume state is cleared when it closes, so reopening after an
abandoned OAuth round trip is not stuck on step 2.
- The OAuth callback shares the session-storage key rather than repeating it.
- SupabaseConnect uses the shared provisioning helpers instead of a fork.
- Restores the doc comment displaced onto TestDataTableResourceQuery.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): simplify Alert layout and balance its vertical padding
The body was rendered by two near-duplicate branches, each wrapping the text in an
extra div only to hang a margin on it, and the margins disagreed: the collapsible
branch spaced above with mt-2, the static one below with mb-2. Since isCollapsed
defaults to true, every non-collapsible alert took the static branch, so titled
alerts read as 24px of space below the text against 16px above -- visibly
off-centre -- with the title and body flush against each other.
Collapse both branches into one and drop the margins; the container's own padding
now sets top and bottom equally, with a small gap under the title row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): only offer Supabase when its OAuth client is configured
The wizard offered the Supabase card unconditionally, so on an instance whose
superadmin never configured a supabase_wizard client -- or whose backend is built
without the oauth2 feature, which compiles the whole /api/oauth router out -- the
card dead-ended at a 404. Gate it on listOauthConnects, the same check
ApiConnectForm already makes, fetched on open so configuring the client mid-session
does not require a reload.
Also drop the Supabase project ref from the existing-project cards: it is an opaque
identifier that means nothing outside Supabase's own dashboard URLs. Show the region
instead, plus a status word when the project is not healthy, since a paused project
is the one case where the connection check fails for a reason unrelated to the
password.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): run the Supabase OAuth leg in a popup
A full-page redirect unmounts the wizard, so anything the user does on Supabase's
side -- signing in, confirming an email, browsing their dashboard -- leaves them
with nothing pointing back at Windmill, and the wizard had to park its state in
sessionStorage to survive the trip.
Open the connect endpoint in a popup instead. The modal stays on screen throughout
and the callback hands the token back through postMessage rather than navigating.
The parked-state path stays as the fallback for browsers that block the popup.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): scope the connection check to the choice that produced it
A failed check stayed on screen when the user switched Supabase mode or picked a
different provider, so a fresh tab opened showing an error about a database it had
nothing to do with. Clear the report and the error on both switches; re-clicking the
tab already selected leaves an error the user is reading in place.
Also polish the Supabase step: project cards get the provider-card treatment (icon,
p-3, flex column) instead of a hand-rolled variant whose block layout left more
padding above the name than below; form labels settle on text-emphasis; and the
signup link sits under the primary button for anyone who does not have an account
yet.
Drop the "free" badge and the "Free on Supabase" line -- every option in the wizard
is free, so neither told the user anything -- and say what the Supabase card
actually does now that connecting an existing project is the default.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(frontend): one setup checklist and one Supabase step for every host
The data table wizard, the instance database modal and the resource drawer each had
their own version of the same two interactions, and they had already begun to drift:
the wizard's Supabase resource shape was rebuilt by hand in the drawer, and the
instance checks rendered with no notion of a step being in flight.
SetupChecklist replaces LoggedWizardResult, whose only consumer was the instance
modal. It adds the running state that component lacked, so a list driven by an
endpoint that reports nothing until it returns still shows where it is. Both the
instance checks and the Supabase provisioning stages render through it.
SupabaseProjectStep owns picking or creating a project, and useSupabaseOauth owns
the popup leg. Each host keeps only what is genuinely its own: the wizard saves a
variable and resource then verifies the connection, the resource drawer fills in its
own form. Both trigger authorization themselves, so a host can offer it a screen
earlier than the step does.
The lists load behind a spinner because which mode to open on depends on whether the
account has projects; deciding that after rendering flipped the toggle under the user.
Adds a kitchen_sink playground for the checklist so the animation and every failure
position can be exercised without a backend, a superadmin, or a Supabase account.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): tidy the resource drawer around the Supabase entry point
Connect Supabase was a hand-styled anchor carrying Supabase's brand hex values
rather than a Button, and it sat in a row whose other controls had settled on
unifiedSize md. Making it a Button meant SupabaseIcon had to satisfy IconType, so it
now takes `size` (deriving height/width from it) alongside the string props its other
callers pass.
The manual resource form spaced every field 32px apart and WhitelistIp added another
16px of its own, which read as a gap rather than a rhythm. One gap of 16px, with the
form itself given a little more separation from the description above it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): stop Supabase resources coming up modified when first opened
Resource forms fill in every unset property from the schema as soon as they render,
so a postgresql resource saved without region, root_certificate_pem and use_iam_auth
was dirty -- and had saved a draft -- the first time anyone looked at it. Write them
with the rest of the value.
SupabaseConnect also rebuilt the resource shape by hand instead of using the shared
helper, which is how the pooler host format ended up in two places.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(backend): record where a data table came from and whether setup finished
edit_datatable_config replaces the whole datatables map and DataTable does not deny
unknown fields, so anything the request omits is dropped without a word. origin and
setup_incomplete would have been erased by any unrelated save;
preserve_unmanaged_datatable_fields carries them -- and migrations_enabled, which had
the same problem inline -- forward for entries that already exist, following renames.
setup_incomplete is what lets a row be recorded before the resource it points at
exists, so the wizard can write nothing until the user finishes. There is deliberately
no intermediate state: the setup runs entirely in the browser, so nothing server-side
could advance one.
datatable_health probes every data table at once for the settings page and skips the
incomplete ones, whose resource_path resolves to nothing yet. set_datatable_setup
patches a single entry instead of resending the map. test_datatable_connection_value
checks a connection the caller has not saved anywhere, which the wizard needs before
it has written a resource.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): make destructive default and subtle buttons read red
Both variants were neutral until the pointer arrived, then filled solid red: nothing
marked the button as destructive until you were already on it. They now carry red text
at rest, with a faded red border on default and a light red wash on hover, which is
what the legacy red border style in the same file had always done.
Three call sites passed color="red" alongside a design-system variant. getStyleClass
returns before colour is read for accent, accent-secondary, default and subtle, so the
delete-migration control, its modal confirm and the import-database button had all been
rendering neutral. They pass destructive now.
The dropdown variant strips the button's own border, and matched border-border-light
literally -- a class the destructive style no longer contains.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(frontend): rebuild data table setup around a read-only row
The wizard gathers intent over two steps, reviews it on a third and writes nothing
until Finish, so a billable Supabase project is created only once the user has seen
what will happen. runSetup is also the retry: every step probes for its own result
before doing anything, so running it again on a half-finished data table resumes
instead of duplicating. Its steps are keyed rather than dispatched on their titles,
where rewording one changed what it did.
The settings row stops being an editable form with a dirty/save cycle. It carries the
name, where the database came from, a health dot and two actions; everything rare
moved into the gear panel, which also offers Finish setup for a data table whose
wizard never completed. Manage is ExploreAssetButton, the control the ducklake list
already uses, and the row and panel both link out to the underlying resource.
supabaseResourceValue no longer assembles the pooler host from the region.
aws-0-<region>.pooler.supabase.com is wrong for any project Supabase allocated
elsewhere, so the host, user and port come from the pooler config endpoint.
Two data tables sharing one database also share _wm_migrations, which is probed
unqualified, so the review step warns when the database being connected is already
behind another data table.
SupabaseConnect is deleted. The resource drawer uses the shared project step
restricted to existing projects: creating one is a billed action and belongs in the
wizard, which has somewhere to report what it did. The kitchen_sink checklist
playground goes with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): fall back to a direct Supabase connection when the pooler cannot be read
Reading a project's Supavisor config needs the database_pooling_config_read scope, which
an instance's Supabase OAuth app may never have been granted. No retry recovers from
that, and the wizard treated it as fatal: the user was left with an error and no way to
finish connecting a project that was otherwise fine.
resolveSupabaseConnection replaces the bare pooler read everywhere it happened. Asking
for session pooling and failing now yields a direct connection plus the reason, which
supabaseResourceValue already knew how to write. Nothing about the fallback is silent --
direct is IPv6-only, which is the whole reason session pooling is the default -- so the
wizard warns on its review step and the resource drawer says so in its toast.
The row is recorded before credentials are saved, so an origin claiming session pooling
has to be corrected once a direct host is what gets written; the run patches it through
set_datatable_setup rather than leaving the panel to report a mode nothing uses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(frontend): open the database behind a data table, and say when it cannot write
Every database in the list now opens the surface that owns its credentials. A postgres
one opens its resource in the editor drawer; a Windmill instance one opens the instance
modal, which is where its setup checks, password rotation and drop already lived. Both
are reachable from the row and from the panel's provenance list, and the provider icon
moved inside the button so the whole thing is one target.
CustomInstanceDbWizardModal targeted #content unconditionally, which put it underneath
the panel drawer that now opens it. It takes a target, and the panel portals it to the
body.
The status column gains a third state. The probe reports privileges but nothing gated
the dot on them, so a data table whose role cannot create tables showed as Connected and
only failed when someone ran a migration. It reads "Limited permissions" instead, and
opens the panel on the report carrying the GRANTs that fix it -- the settings page has
already probed, so the panel takes that report rather than asking the user to run Test
connection over work already done. fullyPrivileged is exported from the report component
so the dot and the report cannot disagree about what counts as healthy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* revert(frontend): keep the data tables settings table as it was
The settings table and the setup wizard are two changes that only shared a file. Splitting
them makes each reviewable: this branch keeps the wizard, and the read-only row, gear
panel, health probe and clickable databases move to their own branch.
The rows go back to the editable form with its pickers and save footer, still opening the
wizard from Add a database. DataTableSettingsPanel, dataTableHealth and dataTableOrigin
had no other consumers and go with them; the connection report stays, because the wizard
shows it too.
DataTableSettingsType keeps `origin`: the wizard writes it, and the review step reads it
back to warn when two data tables would share one database and therefore one
_wm_migrations table.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): confirm before dismissing the data table wizard mid-setup
Closing was guarded while a run was in flight and unguarded before one, which is backwards:
a run leaves a row to resume from, whereas a backdrop click on the review step threw away
the project, the pasted password and the folder with nothing to recover them from.
Backdrop, Escape and the close button now go through one path that asks first. It only asks
when there is something to lose -- no provider chosen yet, or a run that already produced a
result, closes immediately -- so the dialog does not become something to click through.
Continue in the background still leaves in one click; that exit was always the deliberate
one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): stop the wizard claiming the resource folder controls who can use a data table
"Who can use this database" was wrong. Every path that resolves a datatable:// reference --
both executors and the agent-worker endpoint -- reads the resource unchecked, by workspace
and name. A resource in u/admin is usable by everyone's scripts. The folder governs who can
see and edit the connection, and who can reference the resource directly in a SQL step;
neither is who can use the data table. The wizard was contradicting the tab's own
description two screens later.
The folder select and name field become one Path picker, the same one the resource,
variable and script forms use, so the review step reads as a resource path rather than a
permission choice. Its initialPath is snapshotted when the step opens: Path seeds itself
from it, and a live value fights the typing. Finish now also gates on Path's error, so a
taken or malformed path stops the run before it writes anything.
The button that opens all this says "Add a data table" -- the data table is what you get;
the database is a detail chosen along the way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* revert(frontend): move the destructive button restyle out of the wizard PR
This reverts 3881e4d8ea. Making default and subtle destructive buttons red at rest changes
every existing caller of the prop -- the workspace integrations, AI skills, workspace
creation and the instance database drop -- so it is a design-system change, and the call
sites it fixed are the migrations list and the database manager. None of that is the setup
wizard.
Nothing on this branch passes destructive any more, so it leaves with no loose ends.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): make the wizard stepper navigate the steps it already offers
Stepper dispatches a click and paints cursor-pointer on every reached step, but the wizard
never listened, so the breadcrumbs invited a click and did nothing.
They now reach any step already passed, in either direction: going back to check something
should not cost the progress, which means tracking the furthest step reached rather than
the current one. Forward movement still only happens through the primary action, so a step
is never reachable without having been validated -- and changing the intent revokes the
steps ahead of it, or Finish could run against a review built from something the user has
since edited. The five places that cleared the probe on an edit now do both through one
call.
During a run nothing is reachable, and the stepper says so rather than showing a pointer
over steps that will not respond.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): restore the data tables description lost in the branch split
The rewritten description went into DataTableSettings.svelte shortly before that file was
restored wholesale to its pre-rebuild state, so it left with the row rework it had nothing
to do with. The tab went back to describing the plumbing -- a fully managed PostgreSQL
database, reachable from the SDK -- which never answered the question a new user actually
has: why this rather than a Postgres resource.
It leads with what a data table is, then the two things a resource cannot do -- nobody
needs the credentials to query it, and the name can be pointed at another database without
editing anything that uses it -- and closes with what Windmill runs on top. Both middle
claims are the ones every resolution path backs up: datatable:// resolves by workspace and
name, unchecked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(backend): say what is missing when a $res: or $var: reference does not resolve
Both interpolations fetched with fetch_one and mapped the error through to_anyhow, so a
reference to something deleted surfaced as "no rows returned by a query that expected to
return at least one row @workspaces.rs:2169". It names neither the kind of thing that was
missing nor its path, and it is what a data table pointing at a deleted resource reports.
They now fetch_optional and return NotFound naming the path, and datatable resolution adds
the data table on the way out: the caller asked for one by name, and a bare "resource
f/x/y does not exist" leaves them to work out which of them points at it. The health probe
is new, so this string had only just become something users read.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(frontend): gate the data table wizard behind a dev flag
The wizard only appears with `dataTableWizard` set in localStorage; without it the
settings page keeps the inline-row flow it had before this branch, down to the empty-state
copy and the "New Data Table" button, and the wizard component is not mounted at all. The
existing e2e suite drives that button, so the default-off flag is also what keeps it green.
Step 2 of "your own database" becomes one list rather than a segmented control: the
workspace's Postgres resources, then a New resource card that expands in place. A
connection string is not an alternative to a resource, it is how one is written, and the
old layout taught otherwise. The card holds the same connection as a string or as fields
and carries values across when you switch, so `parse` and `compose` have to be inverses --
hence the percent-encoding on both sides, which also fixes a password containing `@`
silently corrupting in the resource form. The Supabase step now uses the same shape.
Names and paths are checked as they are typed rather than at the end of a run that may
have created a billed project first: the data table name against the charset
`edit_datatable_config` enforces, the instance database name against what
`setup_custom_instance_db` will accept, and the resource path against both the resource
and variable namespaces, since the run writes to both and both writes upsert.
`test_datatable_connection_value` refuses `$var:`/`$res:` in its body. It feeds
`transform_json_value_unchecked`, which resolves references with no permission check of its
own, so an admin could otherwise have had the API server decrypt any workspace secret and
hand it to a host the same request chose -- without the audit trail a variable read leaves.
Callers testing something unsaved hold the literal value already.
Alert, SetupChecklist and postgresConnectionString change for everyone, not just behind the
flag: body-only alerts no longer reserve an empty title row, the checklist can nest the
checks a step is made of, and the connection-string parser is shared with the resource form.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: pin ee-repo-ref to the EE branch merged with EE main
The Supabase proxies the wizard calls are still unmerged, so the ref cannot be an EE
main commit yet; it now names that branch merged with EE main rather than the branch
alone, which was nine commits behind and would have been built against a CE main it
never saw.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(frontend): gate the supabase resource path behind the dev flag
* test(frontend): pin connection string parsing to libpq behaviour
* fix(frontend): keep the supabase resource link off the popup callback path
* refactor(frontend): load the supabase resource dialog only behind the flag
* fix(frontend): refuse a resource path the wizard run does not own
* fix(frontend): let a failed data table setup be corrected without losing what it made
* fix(frontend): let a failed setup reuse the resource path it claimed
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(backend): record the two data table connection tests in the audit log
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): use Section for the data table wizard advanced group
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): read connection strings the way libpq does
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(backend): pin the ee ref back to a commit this branch can build
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): keep a failed setup's claims across the redirect and rollback
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(backend): probe a data table with the auth mode the worker will use
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): keep every part of a connection string through the round trip
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(frontend): give a setup run one record of what it created
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): mark a resource claim by edited_at, not its creator
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): mark every claim by revision, and keep an unconfirmed project's secret
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): refuse to test or save behind a connection string that will not parse
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): refuse a connection string carrying options the resource cannot hold
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): allowlist the connection-string parameters a resource can honour
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): guard every created Supabase project, not just the last one
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: three-step wizard for importing a hub project
Importing used to be a single page that inherited whatever workspace happened to
be active, with no way to say where the project should go — the hub cannot know,
since it only ever links to *an* instance. `/projects/import` now asks: which
kind of destination, which workspace, then imports.
Nothing is created, switched or written until the last step runs. The wizard's
state is a plain value in the URL (`importWizard/plan.ts`), so the back button,
the stepper and the Back control are the same operation, and none of them can
strand a half-created workspace — there is no state anywhere else to unwind.
`importWizard/execution.svelte.ts` is the only code that acts on a plan: it runs
create → fetch → import as an observable task list, reuses what already
succeeded when retried, and offers to delete the workspace it created if the run
stops early. Its UI needs — the data table migration review — are injected, so
it holds no components.
The old `/projects/install` becomes a redirect: hubs upgrade on their own
schedule and a self-hosted one may keep pointing at it for a long time.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: let the import wizard survive sign-in and a missing workspace
Signing in with `rd=/projects/import?hub=...` dropped the destination: the login
redirect only honours `rd` verbatim for `/user/workspaces`, so anyone with more
than one workspace landed on the workspace picker instead — the page the wizard
exists to replace, asking the question it was about to ask. Both copies of that
logic now allow the wizard through.
The root layout's "no workspace selected" redirect skips the wizard too. It
picks the destination itself and may end in a workspace that does not exist yet,
so bouncing it to the picker forces the very choice it is there to make.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: bench page for the import project card
/kitchen_sink/import_project_card renders the card against fixtures — a real
project, an oversized one, a minimal one — so its layout can be judged without a
hub running or an import in flight.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): do not warn about renaming an item that does not exist yet
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): make the review step read as one list of what will exist
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): keep the picked Supabase project across the redirect, reject connect_timeout
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: check the data table connection from a worker, not the API server
The wizard's connection check ran on the API server through two endpoints added
for it. That server is a different machine with a different identity, so the
answer was about the API server rather than about the worker that will run the
queries: a host reachable from one is not necessarily reachable from the other,
and IAM RDS and Azure workload identity authenticate as whichever process opens
the connection.
Run the privilege query as a preview job instead. A job goes through the
worker's Postgres executor, which is where `PgAuthMode::of` already picks the
authentication mode, and it takes either a resource value or a `$res:` path
exactly as a Postgres step does. Postgres composes the suggested GRANT
statements through `format('%I')`, so identifier quoting stays where it is
already implemented.
Removes `test_datatable_resource_connection` and
`test_datatable_connection_value`, and `connect_as_the_worker_would` with them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor: fold check_datatable_connection back into its only caller
The helper was split out so the two connection-test endpoints could share a
body. Those endpoints are gone, leaving one caller.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* revert: keep the data table connection check schema inline
It was lifted into components so three endpoints could share it. Two of those
are gone, so it is back to one user and the extraction changes nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: restore openapi.yaml to the branch point
The previous commit restored main's tip rather than the merge base, which
carried three unrelated main-only changes into this branch: the resource
mcp_tools truncation fields, the execution_mode description, and a version bump.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(frontend): drop four effects from the data table wizard
Each was doing work a derived, a load callback or a real entry point does
better.
- The name conflict is kept with the name it was raised for and derived from
it. As an effect it was correct only because it never read what it wrote:
the pre-flight sets the message and the effect does not re-trigger, so adding
a read would have cleared it the instant it appeared. The message now also
comes back if the taken name is retyped, which is what the server will say.
- The default resource selection is seeded inside the fetcher that loads the
list, where "has the fetch settled" cannot be asked wrong.
- Reset-on-open becomes an exported open(), called by the settings page, so a
fresh run is set up by the act of opening rather than by a flag emulating
mount.
- The OAuth connects and the folder list become resources; supabaseAvailable
and folders are derived from them. defaultFolder takes the list rather than
reading it, so the fetch can seed off its own result.
Leaves the debounced path check, which is async with an out-of-order guard.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(frontend): drop three effects from the Supabase branch
- useSupabaseOauth reports success as onAuthed, alongside the failures it
already reported. SupabaseResourceConnect was watching `authed` to find out;
it takes the callback instead, keeping the guard that stops an authorization
started elsewhere on the page from opening its dialog.
- SupabaseProjectStep loads its orgs and projects through a resource keyed on
the token, so the `loaded` latch goes and re-authorizing reloads rather than
keeping the lists from the expired session.
- SetupChecklist records what the user toggled and derives the open state from
it, a failed step defaulting to open. Recording the open state instead needed
an effect to force it, and that effect re-ran on every progress update, so a
description closed while anything was still ticking reopened. A close now
holds for the life of the checklist, including across Try again.
Leaves the message listener, which subscribes to another window.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): confine the modal restyle to the wizard, and trim the comments
The wider side padding and lighter dialog heading were changing all 17 Modal2
dialogs to suit this one flow. They move behind an opt-in `formStyling`, taken
by the three dialogs this branch owns; every other Modal2 renders as it did.
Also drops two comments that cited a design approval rather than a constraint,
and shortens the blocks that had grown past the four lines AGENTS.md asks for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): use the accent token for the wizard's links
`text-blue-500` is the marketing blue `#3B82F6`, which brand-guidelines.md
rules out in the app interface.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: point ee-repo-ref at the EE branch head
Picks up EE main, which the branch now needs, and the Supabase proxy auth fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): read sslmode by name, and stop decrypting a secret to date it
- `sslmode` was found by searching the query text, so it also matched inside
another parameter's value: `?application_name=sslmode=disable` passed the
allowlist on the parameter name and then parsed as a request to turn TLS off,
which both the wizard and the resource form saved and probed. Parsed with
`URLSearchParams` by exact name, with a test.
- `secretMark` read the variable with `decryptSecret` defaulted to true, so
every write decrypted a secret nothing reads and recorded the decryption --
including someone else's on the retry about to refuse it. It wants only
`edited_at`, which is returned either way.
- The probe gave up at 15s while the worker allows its Postgres connect 20s, so
a host that accepts the connection and never answers was cancelled and
reported as a missing worker rather than a failed connection.
- The create-mode region and project name did not report an intent change, so
renaming a project after a name collision left the failure naming the old one.
- Two comments described the code as it was before the claim mark became a
revision, and a doc comment outlived the field it documented.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): read connection parameters the way libpq does
One reader for both the parser and the allowlist, since they disagreed about
what a string says in two ways that both ended in a weaker connection than was
pasted:
- `URLSearchParams.get` takes the first of a repeated parameter and libpq takes
the last, so `?sslmode=disable&sslmode=require` was read as `disable`.
- The allowlist folded the parameter name and the parser did not, so
`?SslMode=verify-full` was refused by neither and honoured by neither, and
saved as the `require` default.
The parked Supabase run is now handed to `open()` rather than read back off the
`resume` prop it was just assigned to, so restoring it does not depend on when
that prop reaches the component.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): keep connection parameter names case-sensitive
libpq does not fold them: `?SslMode=disable` is rejected as an invalid URI
query parameter rather than read as `sslmode`, which a local server confirms.
Folding made Windmill accept and honour a string Postgres itself refuses;
naming the parameter instead tells the user why it cannot be stored.
The last-value-wins rule for a repeated parameter is unchanged, and matches
what the same server does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): seed the Supabase organization from the project it selects
The loader took `orgs[0]` independently of the project it seeded, so an account
whose first project sits outside its first organization had the review step name
an organization the database does not belong to. Picking a project by hand
already derives it; the seeding now does the same.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): let the probe report an empty search_path instead of failing on it
`format('%I', NULL)` raises rather than returning NULL, so a role whose
search_path names no valid schema failed the whole privilege query and was
reported as an unreachable database. That is the one case `fix_search_path`
exists to name, and it never reached the user. Verified against a local server
with `SET search_path = ''`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): say which of the two refusals a connection string hit
Making parameter names case-sensitive gave `unsupportedConnectionParam` two
reasons to refuse, and the single message explained only one. `?SslMode=` was
answered with "Windmill cannot store SslMode on a Postgres resource", which is
false twice over: sslmode is exactly what the resource stores, and the string
asks for nothing because Postgres rejects the URI. It now names the spelling
when the parameter is one we keep, and the storage limit otherwise.
The folder-list guard also still read the `resume` prop that `open(parked)` was
changed to stop trusting, so the resumed path now comes from whatever `reset`
was handed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): leave the Supabase organization unset when the lookup misses
Falling back to the first organization named one the seeded project is not in,
since `supabaseSummary` prefers `intent.org` over the project's own. Unset, it
falls through to the project's organization identifier — the right one, spelled
as a slug rather than a name.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: harden the import wizard and put it on the design system
Review fixes, then the parts of the wizard that were hand-built where the
design system already had an answer.
Correctness:
- Hub SVGs are sanitised with DOMPurify before `{@html}`. The earlier comment
claimed the markup came from the hub's own icon package rather than user
input, which the custom-URL feature makes false: the hub is whatever address
the user typed.
- The run owns navigation while it is in flight. The stepper refuses to move,
`beforeNavigate` cancels browser back/forward, and unmounting resolves a
pending migration review so the executor cannot hang waiting on a component
that is gone.
- The folder edited on the last step reaches the executor, so a retry after
changing it imports where the field now says.
- `validateWorkspaceId` and the workspace-entry pair (`listUserWorkspaces` then
`switchWorkspace`) are extracted, so the wizard and the real create form
cannot drift on what an id is or on what entering a workspace means.
Design system:
- The destination tiles are `RadioCard`, which gains `showRadio` and a snippet
`description`; the wizard turns the glyph off because the border and tint
already say which one is picked. `RadioCard` now also carries `role="radio"`
and `aria-checked`, which it had neither of, and marks its selection with
`surface-accent-selected` — the token `FileExplorer`, `TriggersTable` and
`RunnableRow` all use for the chosen row.
- Form labels follow `brand-guidelines.md` — sentence case, real `<label>`
elements so the text focuses the field, Caption-styled errors — rather than
one-off 11px uppercase tertiary text. They use the lighter secondary weight,
since the fields arrive prefilled and the value carries the meaning.
Folder choice, restored and merged:
- Picking an existing folder came back for an existing-workspace destination.
`FolderPicker` takes a `workspace` prop so it can list a workspace without
switching to it, and resolves `whoami` there — its write flags came from
`$userStore`, i.e. the wrong workspace, which rendered every real folder
read-only and unselectable. A new workspace has no folders to choose between,
so it is not asked.
- The progress list and the imported paths are one component: the paths hang
off the import task that produces them instead of forming a second list.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: address review — history, workspace restore, SVG styles
Two blockers and the findings around them.
- The wizard pushed history entries for navigations the user did not ask for.
`folder` initialises to the project slug while the plan holds none, so the
mirroring effect fired on mount and pushed a URL differing only by `&folder=`;
pressing Back returned to the entry without it, which re-fired and re-pushed.
Back could never leave step 3. `go` now takes `{ replace: true }`, used by that
effect and by the step guard — the two navigations the page decides on its own.
The comment claiming `go` replaced was written without checking that `goto`
forwards to SvelteKit, which defaults `replaceState` to false.
- Undoing a run left the app pointing at the workspace it had just deleted:
`#ensureWorkspace` switches in, `deleteCreatedWorkspace` deleted without
switching out. The dead id was persisted on the next navigation, `getUserExt`
then returned undefined, and the following reload logged the user out. The
executor now remembers where the app pointed before it started and puts it back.
- `FORBID_TAGS: ['style', 'image']` on the hub SVGs. The profile allows both; an
inline `<svg><style>` is document-scoped, so a hostile hub could restyle this
page — including moving the wizard's own Import and Delete controls — and
`<image href>` is a beacon. The doc comment asserted a guarantee the config did
not deliver.
- The existing-workspace id is validated like the new one and encoded where it is
interpolated into `/api/w/<ws>/...`; it arrives from the URL exactly as the new
one does and ends up in `workspaceStore`.
- `AppConnectInner`'s two RadioCards get a `role="radiogroup"` wrapper, since they
now carry `role="radio"` and a screen reader cannot place a radio without one.
- `FolderPicker` records a created folder against the membership it is reading, and
before reloading, so a non-admin can re-pick the folder they just made in another
workspace instead of finding it `(read-only)`.
- Step 3 shows trigger and data table migration counts once the export is fetched.
The page this replaced showed them, and the warning underneath talks about
triggers the user was never told about.
- First tests for the two pure modules: the workspace-id contract the wizard and
the create form must not drift on, and the plan/URL round trip the whole wizard
rests on.
- Doc fixes: the retry claim (the granularity is the task, not the item), the bench
header, a fractional `?step=`, an empty name in the destination card, and the
three copies of one rationale AGENTS.md asks to state once.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(frontend): pin which refusal a connection string gets
The two messages differ in what they ask the user to do, and the condition
choosing between them — whether the lowercased name is one the resource keeps —
is not visible from either call site. `Connect_Timeout` is the case that keeps
them honest: miscased *and* unstorable, so respelling it would not help and the
message must not suggest it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): hand a failed Supabase leg back to the page holding its run
Denial, a token error and a malformed callback all sent the user to
/resources whether or not a run was parked. Nothing else consumes the park, so
the run stayed in sessionStorage and sprang the wizard open on an unrelated
later visit instead. A parked run now lands on the data tables tab, where the
wizard resumes on the setup step and can authorize again.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): let a run reuse the name of a row it could not take back out
`removeRow` reports `kept` when the undo cannot reach the server, so the row
this run wrote stays in the workspace config and comes back in `existingNames`.
The client-side name check then refused the retry on the run's own name, with
no way forward but a rename. The instance database name has carried the same
exemption since it was written; this is the data table name catching up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(frontend): discard a variable check the wizard has moved on from
The post-await guard compared only the path, and the path is built from the
review step's fields -- so picking an existing resource stops the wizard minting
one without changing it. A check already in flight then answered for a branch
nobody was on, and a `true` disabled Finish over a path the run no longer
writes. The cleanup cannot help: it cancels a pending timer, not a live request.
Both sides of the await now ask the same question.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 483513b70979aa9497cab869837108d948449984
This commit updates the EE repository reference after PR #715 was merged in windmill-ee-private.
Previous ee-repo-ref: 8604b30a740c5620069208801a7ae50937b61977
New ee-repo-ref: 483513b70979aa9497cab869837108d948449984
Automated by sync-ee-ref workflow.
* feat: a setup step for what the import cannot bring with it
A project's data tables and credentials cannot travel with it: a data table is a
named database connection the workspace owns, and resource values are secrets the
hub never publishes — `importResourceStub` creates every one of them empty. The
wizard used to state that as a dead end. Mid-import it asked the user to cancel,
create the data tables by hand and start over, which for a *new* workspace was
every single time, since a new workspace has no data tables at all.
Step 4 replaces that. It appears only when the run leaves something undone, lists
what that is, and does it in place: a Postgres resource per missing data table
(one merged `editDataTableConfig` write, then the migrations), and the existing
resource editor for each credential. Skipping is allowed and says plainly which
parts of the project will not run.
It is self-sufficient from `workspace` + `slug` — it re-fetches the export rather
than reading the executor — so reloading on it works and the plan in the URL stays
the whole state. Rows are marked done rather than removed, with SaveButton's
confirmation flash, because a checklist line that vanishes when completed reads as
something going wrong.
Two things the step needed from elsewhere:
- `ResourceEditorDrawer` gained `onSaved`. `onRestored` fires only when an old
version is restored, so a caller showing state derived from the resource had no
way to know a save had happened — the row kept saying "missing token" after the
token was filled in.
- The run now loads the destination's membership into `userStore`. The wizard's
page is reparented out of `(logged)` and never gets that layout's `getUserExt`,
so anything asking what the user may do reads "no user" and refuses.
`applyOneMigration` is exported for the same reason the step exists: the import
skips a migration whose data table is not configured, and this is where it is not
skipped any more.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: set up data tables through the wizard, not a hand-rolled form
The setup step drove `editDataTableConfig` itself, which meant it could
name a table and record migrations but could not create the database
behind it — the case a brand-new workspace is always in. It now opens
`AddDataTableWizard`, which owns that whole path.
Four additive props carry what the import flow needs and nothing else,
so `DataTableSettings` is unchanged:
- `initialName` — the migrations only apply to a table of the name they
target, so the wizard opens on it. Still editable.
- `modalTarget` — `#content` is the `(logged)` shell's scroll container,
and the import page reparents out of it, so the portal would find
nothing and the dialog never appear.
- `finishAlso` / `onFinishAlso` — running the migrations was invisible
until it had already happened. It is now named on the final button
("Create data table and run migrations") and reported as the last row
of the wizard's own checklist, failing there rather than silently.
Rows are marked done rather than removed, so the list still says what
was set up. Resources keep their card and swap "Fill in" for "Saved".
`Finish` is the primary and stays disabled until nothing is outstanding;
`Skip for now` sits beside it, and the info alert explaining the skip
turns into a success one when everything is configured.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: show each credential's own integration icon, and cut the data table blurb
The credentials list marked every row with the same key glyph, so the only
thing distinguishing them was the path. `IconedResourceType` renders the
provider's own mark from the resource type already on the row, falling back
to a generic box for types with no icon.
The data table explanation said "a data table is a database this workspace
owns" directly under a label reading "Data tables to set up", and "this
project ships with one it expects to find" directly next to the count that
says so. Both halves went; what a data table is *for* and what to do next
are what a first-time reader needs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: build the Google sign-in button from the design system
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtYCxXEn2WujwVvh5aZRCa
* fix: qualify a data table FK target with its schema
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtYCxXEn2WujwVvh5aZRCa
* fix: confirm before skipping an unconfigured data table
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtYCxXEn2WujwVvh5aZRCa
* fix: show a loader while the wizard hands off to the workspace
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtYCxXEn2WujwVvh5aZRCa
* fix: resume an import whose workspace was already created
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtYCxXEn2WujwVvh5aZRCa
* refactor: draw the import run with SetupChecklist and ask before leaving it
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtYCxXEn2WujwVvh5aZRCa
* fix: portal the setup step's confirmation above the data table wizard
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtYCxXEn2WujwVvh5aZRCa
* fix: supply the three APIs the import wizard already calls
`AppConnectDrawer`, `ImportProjectStep` and `execution.svelte.ts` landed
calling into props and exports that were never committed alongside them,
so the branch did not type-check. Each half is here now:
- `AppConnectInner.fillPath` — connect into a resource that already
exists instead of refusing the path. The import creates every resource
as an empty stub, so without it the connect flow can only ever say
"already exists, delete it or pick another path". Opt-in: unset, the
flow still refuses to write over anything, which is what `ResourcePicker`
and the resources page rely on.
- `ProjectContentBadges.contentSummary` — the badge counts as one line of
text, for the import step's task row. Shares `kinds()` with the badges
so a project cannot be counted two ways.
- `installProject.onMigrationsStart` — fires before the reviewed
migrations run, which is the only signal that phase has begun; the
import step draws them as their own checklist row off the back of it.
Also fixes the wizard wedging itself shut: `requestClose` set `dismissing`
and cleared it after awaiting the confirmation, so an `ask` that threw left
the flag set — and the backdrop, Escape and the close button all return
early on it, leaving a reload as the only way out. Now `finally`, plus a
reset on open, since a promise that never settles never reaches `finally`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: offer Connect wherever the connect dialog would actually work
The setup step decided a resource was connectable by looking only at the
instance's configured OAuth clients, while the dialog it opens also accepts
a provider the registry marks client-credentials-capable — those carry their
credentials per resource, so no superadmin has to configure anything. The
two disagreed for bitbucket, coupa, linkedin, servicenow, spotify, visma,
xero and zoho: the step showed "Fill in" where the dialog would have
connected.
Rather than copy the predicate, `oauthRegistry.ts` now owns it, and
`AppConnectInner` reads it from there. That folds in three lookups of the
same registry that had drifted apart inside the component — `registryEntry`,
`isCcCapable`, and a raw index at the connect-template site — so the sandbox
suffix rule is written once instead of twice.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor: draw the project card's icons from the ones we already ship
The card fetched each integration icon from the hub as SVG markup, sanitized
it and injected it with `{@html}`. The hub renders those icons out of
`@windmill-labs/components` — this frontend's own package — so it was a
cross-origin round trip to get our own assets back, and it made the card
depend on a read that a hub with `API_SECRET` set refuses outright.
`hubAppIcon` resolves them through `appIconComponent` instead, so they are
components again: no fetch, no DOMPurify, no `{@html}`, and they paint on
first render rather than after a round trip. Integration icons now show even
against a gated hub; only the summary and the uploaded logo still need it.
The one thing the hub was doing for us was resolving `postgres` to the
`postgresql` mark, which its `aliasApp` bridges and our icon map does not —
so that single alias comes along, next to a note pointing at its counterpart.
`ImportProjectSummary.hub` goes with it: it existed to build icon URLs and
nothing read it afterwards.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: two regressions this branch introduced into shared drawers
Found auditing the files here that are used elsewhere in the app.
`AppConnectDrawer`: the guard added to stop the inner component being opened
twice compared the last resource type against the current one, and reset it
to `undefined` on close. The resources page opens the drawer with no resource
type, so both sides were `undefined`, the guard matched, and the second
opening never handed off — the type list came up empty. The drawer destroys
its content on close, so this hit every reopen. Now a flag armed per `open()`
call, which cannot collide with a resource type.
`ResourceEditorDrawer`: adding `onSaved` had turned the Save handler into
`await save(); closeDrawer()`, so the drawer stopped closing immediately and
waited for the write. `save()` catches its own errors and never rejects, so
that was pure added latency for all ten callers. It now starts the save,
closes as it always did, and awaits only to fire `onSaved`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: keep the destination through a first-time signup
Someone who follows a shared hub project without an account signs up, and the
OAuth callback sends a first-time user to onboarding — dropping the `rd` it
had already read out of localStorage. They finish onboarding in an empty
workspace with no sign of what they came to import, and have to go back to the
hub and click again. That is the path this feature exists for.
The callback now passes `rd` on, and onboarding's two exits honour it instead
of hardcoding `/user/workspaces`. Same-origin relative paths only: `//host` is
a valid URL that leaves the origin while still starting with `/`, so the guard
rejects it rather than bouncing a fresh account off-site.
Nothing changes for a signup without `rd`, which is every existing one.
Gets the user to the wizard with the project in hand; they still pick a
destination on step 1. Having onboarding create the workspace and hand into
step 3 is the larger version, not done here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: address review — username, name length, leaving mid-run
**The new-workspace username was never validated.** Step 2 shows the field when
the instance does not derive one, but neither the Continue gate nor
`planProblem` looked at it. `create_workspace` does not close that hole:
`nw.username.ok_or(...)` accepts `Some("")` and never runs the `VALID_USERNAME`
check `join_workspace` does, so a cleared field created a workspace whose owner
has an empty username, and a digit-first one was stored verbatim. Both now
refuse, using the same `validateUsername` the sibling creator has always run.
**The name length was unchecked**, so a >50-char name walked through two more
steps and failed at create. `WORKSPACE_NAME_MAX_LENGTH` sits next to the id
limit and `planProblem` checks it.
**Leaving mid-run did not stop the run.** The dialog promised "The import stops
where it is. Coming back to this link picks it up again", but navigating away
only unmounted the UI: the executor kept going, reached `done`, and called
`clearParkedImport()` — so returning to the link tried to create the workspace
again and failed with "already exists". Worse, the review drawer's teardown
resolved the pending review to `false`, meaning "skip the migrations", and the
orphan imported every item without the tables they need.
Nothing can abort a request already in flight — `installProject` takes no
signal — so `abandon()` stops the run at the next phase boundary and leaves the
workspace parked, and the teardown now resolves `'abort'`, which stops the
import rather than silently dropping the migrations.
Also drops a stale JSDoc above `hubAppIcon` still describing the fetch-and-
sanitize implementation that `ea31f73ed3` replaced.
Adds the coverage the review asked for: the parking decision at the end of a
run, and the two validation gates.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: address review round 2 — XSS, retargeting, abandonment, redirects
**Hub data-table names were inlined as raw HTML.** `skip()` built the
confirmation body as an HTML string, and `createAsyncConfirmationModal` renders
`children` through `createRawSnippet`. A `datatable_name` comes straight from
the hub export, and a hub is not necessarily ours — `hub_base_url` is an
instance setting — so one carrying an event-bearing element ran script in this
authenticated origin. Escaped. Same class as the round-1 SVG finding, in a
different sink.
**The setup step read unretargeted resource paths.** `installProject` rewrites
every resource into `f/<folder>/`, but the step re-fetched the raw export and
used its paths verbatim. Importing into a folder other than the slug made
`getResource` throw for every stub, the catch skipped them, and the step
reported "You're all set" over credentials nobody had filled. It now retargets
the same way the import did, and filters to the import folder — the containment
guard the installer applies, so a crafted export cannot name a path outside it
and get offered for editing.
**Abandoning only stopped between phases.** `installProject` takes a `stopped`
callback now, checked before every write loop, so leaving mid-run stops the
remaining items instead of just the remaining phases.
**A failed setup migration reported success.** `runMigrationsFor` swallowed the
error, so the wizard marked its "Run migrations" step done and closed over a
failure — leaving the data table name taken and no way back to retry. Rethrown,
which is what the wizard's checklist reads.
**`onboardingDestination` used a weaker redirect check.** `/\evil.com` passes
`startsWith('/') && !startsWith('//')` but WHATWG URL parsing resolves it to
another origin. Replaced with `toSameOriginRelativePath`, which already rejects
that, control characters and oversized values.
**Two workspace ids reached step 3 that the backend refuses:** a blank one (the
Continue gate never required `id.trim()`) and `global`, which
`check_w_id_conflict` rejects outright while `existsWorkspace` reports it free.
Also: `size="xs2"` → `unifiedSize="2xs"`, and two doc comments reattached to the
functions they describe.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: address review round 3 — both regressions from round 2
**`resume` never rejected a run from a different plan.** The seed computed its
tag from the plan being rendered, so `run.key === planKey` was true by
construction and the guard could not fire — the comment claimed the opposite.
Finishing an import into workspace X, stepping back to pick workspace Y, then
returning showed X's finished checklist against Y's plan, with a Continue
button, over an import into Y that never happened. `ImportExecution.planTag`
now carries the plan the run was made for, and the seed uses that.
**Abandoning mid-import still reported `done`.** `installProject` returns early
when `stopped` goes true, and it returns exactly as it does on success, so the
tail of `#import` could not tell the two apart: a run stopped after 3 of 10
items wrote `import: done — 3 items`, no error, `done = true`. Since the page
hands that run back on return, the primary button became Continue rather than
Retry and the seven skipped items were silently lost — breaking the promise the
leave dialog makes. The tail now checks the flag and leaves the run failed and
retryable.
`abandon.test.ts` was a hand-written copy of the parking decision, which is why
it guarded neither. It now drives a real `ImportExecution` with the install seam
mocked, abandons from inside the write loop (the only way it happens — `run()`
clears the flag on entry so a retry can proceed), and asserts `done`, the error,
and both parking outcomes. `planTag` is covered too: different destination,
different project, and that the editable folder does not change it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: address review round 4 — unreviewed SQL, premature finish, stale Back
**Setup ran hub SQL nobody had seen.** Step 3 reviews the migrations it can run
there, but the ones deferred to setup went straight to `applyOneMigration`
against whatever database the wizard was pointed at — which can be an existing
resource holding unrelated objects. Each unconfigured row now carries a
disclosure showing exactly what will run, before "Set up" runs it.
**Finish was live while the setup decision was still outstanding.** For a project
with migrations but no resources, `execution.done` exposed the button while
`listDataTables` was still in flight and `setupNeeded` was still false — clicking
in that window left for the workspace and skipped a step the answer, a moment
later, said was needed. It now reads "Checking…" and is disabled until the check
settles.
**A reload on step 4 turned Back into a re-import.** `resume` only carries the
page's in-memory execution, so after a reload Back mounted a fresh step 3
offering Import over a bundle already in — and on a new workspace, a create that
now fails because the finished run cleared its parking. Back exists only while
the page still holds the run, which excludes exactly that case.
**`validateWorkspaceId` over-rejected a fork named `global`.** It reaches the
backend as `wm-fork-global`, which is accepted; only the effective id is checked
now, so a plain `global` is still refused. Covered by a test.
**An abandoned run left the migrate row spinning.** It is appended once the
review settles and set running by `onMigrationsStart`; stopping before its loop
left it on `running` forever, reading as work still in progress on a run that
had stopped.
Also moves the `run()` contract back onto `run()`, and gives `ImportSetupRow` an
optional `extra` snippet for detail that does not fit on one line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: close the other two routes back into a run-less import step
Round 4 gated the setup step's own Back button when the completed run was no
longer in memory, but that is the least used of three ways back into step 3, and
both reviewers landed on the same gap.
The stepper renders every earlier step as reachable, and `importIsRunning()` is
false after a reload, so its "Import" tab walked straight there. And `onFinish`
pushed step 4 over step 3, leaving the browser's own Back pointing at the same
place.
After a reload there is nothing to hand back: the executor was in memory, and a
clean finish clears the parking, so step 3 mounted with `resume` undefined and
offered a fresh run — re-importing a bundle already in (a wall of path
conflicts), or on a new workspace re-running a create that now fails as already
existing, with no Delete offered because that execution never made it.
`ImportWizardSteps` takes a `lowestStep`, which the page raises to 4 exactly
when the run is gone, and the step-3 → 4 transition replaces rather than pushes.
Verified against a real reload: the stepper stays on step 4 and says why, and
browser Back lands on step 2 with no runnable import.
Also adds the migration-phase abandonment assertion the review asked for — that
no task is left on `running` when a run stops.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test: make the migration-phase abandonment test actually reach it
It asserted over a branch it never ran. The mock `installProject` never called
`onMigrationsStart`, and with `migrations: []` in the export and
`reviewMigrations` returning nothing, `#import` never appended the `migrate` row
at all — so "no task is left running" was true because no task existed. The
comment was wrong too: the real `onMigrationsStart` fires at the head of the
migration loop, past every item loop, not at the start of the writes.
The mock now mirrors that order — item loops, then `onMigrationsStart`, then the
migrations, with `stopped` checked before each write — and a second hook lets a
test abandon after the row is running. The export ships a migration and
`reviewMigrations` returns it, so the row exists to be pinned, and the test
asserts it exists before asserting its status.
Checked by removing the fix: it fails with `expected 'running' not to be
'running'`, and passes with it restored.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor: ask the instance what exists instead of remembering it
The wizard kept a note in `sessionStorage` — "this run created workspace X" —
so a reload could tell that a create had already happened. A note is a second
copy of a fact the instance already holds, and it could outlive the workspace
it named: the comment on `createdWorkspace` said a parked id might point at a
workspace someone else made at that id afterwards, and that there was no way to
tell, because a workspace carries no discriminator.
It carries `owner`. It is set to the creator's email at `INSERT INTO workspace`,
`listWorkspaces` already selects it, and the generated `Workspace` type already
has it. So the question the note was answering can simply be asked:
`probeWorkspace` returns whether a workspace with the plan's id exists among
the caller's, and whether they own it. Ownership is what makes adopting one
safe — an id that exists but belongs to someone else is not this run's work.
`parking.ts` and its test are gone. Nothing in the wizard writes storage now:
the plan is in the URL, what exists is in the instance, and what is in flight is
in memory, which is where in-flight things belong.
`probe.ts` also carries the two reads the follow-up needs — which of the paths
an import would write are already there, and whether a migration's tables exist.
The second is the ground truth for "did this migration run", covering both paths
`applyOneMigration` takes: it records a migration when the data table has them
enabled, and otherwise runs the SQL as a job nothing remembers. The tables
outlive both. It returns `undefined` rather than `false` when it cannot tell,
since "not there" invites a caller to run the migration and "cannot tell" does
not.
Verified against a real reload mid-run: the second attempt makes no
`createWorkspace` call, one `workspaces/list` call, and carries on to the fetch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: import only what the destination does not already have
A retry resent the whole bundle. Everything that had already landed came back
as "already exists" — nine failures over work that had succeeded, with no way
to tell those from the ones that genuinely failed. The same thing happened
importing into a workspace that already held some of the project.
`installProject` now takes `alreadyPresent`, checked after retargeting because
that is what the items will actually be called, and `probeImportedPaths` fills
it from the destination on every run. On a workspace the run just created the
answer is empty and nothing is skipped, so this costs four scoped reads and
changes nothing about a first import.
Skipping is not replacing. An item that is there is left exactly as it is —
the same promise `updateIfExists: false` already makes for a resource whose
value someone has since filled in.
`InstallResult` gains `skipped`, because "already there" is neither an import
nor a failure and reporting it as either is a lie. The checklist still lists
every item the project ships; a skipped one shows as skipped and says why. The
import row now counts the three outcomes separately — `8 already there` rather
than a green tick over `2 apps, 4 scripts, 2 resources` it did not write. That
last part needed the pre-run breakdown to stand down once the run has an
outcome of its own, or it went on claiming the import had happened.
Checked by removing the gate: two of the four new tests fail. Verified against
a real backend by re-importing Calendly into a workspace that already had it —
0 failures, 0 create requests, and the row reads "8 already there", where the
same run previously produced 9 conflicts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: skip triggers that are already in the destination
probeImportedPaths asked about scripts, flows, apps and resources but not
triggers, so a retry replayed every trigger create into an API that rejects
an existing path — reporting a failure for something already there, which
is the wall the presence probe exists to remove.
Triggers have no prefix-filtered list endpoint, so they cost one call per
kind; the probe only asks when the project actually ships triggers.
The presence set is now keyed by kind as well as path. The five kinds share
one f/<folder>/ namespace, so a trigger and a script may both be called
f/cal/sync, and a flat path set would let either one mask the other.
Also drops expectedPaths, which was exported and tested but never called.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx
* fix: don't let a failed migration read as a finished setup
Three linked gaps around the step-4 data table setup:
Finish setup was clickable while the step was still loading. Empty rows and
blanks made outstanding === 0, which reads the same as having nothing to do,
so a quick click left the wizard before the missing data table was even
discovered. Skip already guarded on loading; Finish now does too.
When the data table wizard's appended migration step failed, run.result kept
runSetup's successful verdict, so the primary action offered Done over a
failed row and closing raised no warning. The failure is now tracked apart
from run.result, and Try again re-runs only the appended step — re-running
the setup would ask for the table name it just took and be refused.
A failed row in the import step reopened the full wizard, which rejected the
name it had itself created, leaving no way back to the migration that
actually failed. Such a row now offers "Run migrations again" instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx
* fix: read the destination's real state instead of inferring it
Three ways step 4 could report work that had not happened:
AddDataTableWizard wrote through $workspaceStore while ImportSetupStep used
the workspace from the URL plan. The import page is reparented out of
(logged), so nothing re-runs the layout's workspace persistence; after a
reload the store still named the workspace the user came from. "Set up"
would then create the data table there and run the migrations in the
destination. The workspace is now a prop, defaulting to the store so every
other call site is unchanged.
load() marked a row done whenever the data table name existed. The wizard
creates the table and the migrations run after it, so a table can be there
with none of the project's tables inside it — and a reload rebuilds rows
from scratch, hiding the failure. It now asks probeMigrationApplied, which
already existed for exactly this question. An undefined answer ("cannot
tell") keeps whatever the row said rather than inventing an outstanding row.
A reviewed migration could fail in step 3 while the run still reported a
clean finish: the migrate row said failed, but `error` was set only from
item failures, and `error` is what offers Retry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx
* fix: keep the migration retry reachable after a reload
The retry-only action needed two things the reload did not have. load()
read the destination's data tables into a local set and dropped them, so
configuredNames was empty and the branch could not fire; it now seeds
configuredNames from the call it already makes.
And the branch keyed on the row saying `failed`, which only holds while the
failure is still in memory. A reload rebuilds every row from scratch, so the
same situation reads as `unconfigured`. It now keys on the data table
existing while its tables do not, which is the same state either way.
Without both, a reloaded failure sent the user back into the wizard, which
refuses the name it created — no way to reach the migration that failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx
* docs: state the constraint, not how the code got here
AGENTS.md: "Describe the code as it is, never its drafting history". Nine
comments across the wizard narrated what an earlier iteration did — "used
to remember", "The regression:", "would otherwise warn" — which says
nothing to a reader who never saw it. Each now states the durable reason
directly: what the code must hold to, and what breaks without it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx
* fix: page the presence probe, and keep a failed run retryable
probeImportedPaths called each list endpoint once. They paginate at 30 rows
by default, so it answered correctly for a small project and silently
under-reported a large one — every item past the first page went back
through a create call that rejects an existing path. It now pages at 100
until a short page, with a 100-page stop so an endpoint that never returns
one cannot loop.
And a run that finished with failures offered only Finish. `done` is what
the step reads as terminal, not `error`, so a failed migration left no way
to run the SQL again. Retry now sits beside Finish whenever the run reports
an error — beside rather than instead, so a migration that fails every time
cannot trap the user short of step 4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx
* fix: don't offer to discard a data table that was created
Closing the wizard after a failed appended step asked "Leave without adding
a data table?" and warned that what ran had left things behind. Every part
of that is false when the setup itself succeeded: the data table exists and
works, and only its migrations did not run.
hasUnfinishedIntent() now asks only whether the setup succeeded. The import
step is the only caller that passes onFinishAlso, and it shows that failure
on its own row with a way to run it again, and will not let Finish through
while it stands — so closing loses nothing.
The in-dialog "Try again" is unchanged; it is still the direct retry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx
* fix: address round 8 — trigger kinds, migration retry target, unknown state
Four findings, two of them real defects in this branch's own work.
The presence key flattened every trigger kind into `trigger`. Each kind is a
separate table keyed on (path, workspace_id), so a workspace can hold a
schedule and an HTTP trigger both called f/cal/sync; whichever existed
answered for the other and the second was reported "already there" without
being imported. The key now carries the kind, which both sides already had.
projectInstall's own doc makes this argument for the five top-level kinds —
it just stopped one level short.
The wizard's in-dialog "Try again" ran runMigrationsFor(wizardFor), but
afterWizard() clears wizardFor as soon as the failed run reports, while the
dialog stays up. It resolved against no row and the step was marked done
over SQL that never ran. The target is now held separately, and an unknown
name throws rather than resolving — a resolved promise is what the appended
step reads as success.
settle() resolved "cannot tell" to done exactly on the reload it was written
for. A data table whose database is unreachable read as Configured and the
step said "You're all set" over a project whose apps fail on open. There is
now an `unknown` state that says so and still counts as outstanding. It also
asked for one full schema per migration; migrations for one data table all
target the same schema, so probeMigrationsApplied reads it once.
run()'s doc still described the pre-probe retry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx
* fix: address round 9 — abandon during the probe, and copy that outlived it
Abandoning while probeImportedPaths was in flight returned without settling
anything. `import` goes running before the probe is asked, so the checklist
kept a spinner on a run that had stopped, beside an enabled Retry and with
no explanation. The settling the post-installProject path already did is now
a helper both paths call.
Three pieces of copy still described the behaviour this branch replaced:
the resource alert said an existing path is "reported as failed" when the
probe now leaves it alone and reports it as already there; and the step-4
footer and skip confirmation both told the user to set up a data table that
the new `unknown` state means they already set up — only its schema could
not be read. Those two now branch, so the strong warning stays strong for a
data table that genuinely does not exist.
The presence-key doc named `trigger:http_trigger`; WorkspaceTriggerKind has
no such value. It is `http`, in the comment and in the two test mocks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx
* fix: never rerun SQL whose applied state is unknown
`unknown` covers two different unknowns, and this treated them as one. The
schema could not be read, or the SQL names no table `expectedTables` can
resolve — and the second is arbitrary published SQL, which may carry a
non-idempotent INSERT or ALTER. The row offered "Run migrations" and the
footer claimed rerunning was safe; both were claims this code cannot make.
An unknown row now offers "Check again", which re-reads and executes
nothing. That settles the case which actually recovers — a database briefly
unreachable — and leaves Skip, which states the uncertainty, as the way past
one that does not.
The partitions behind the copy also missed `failed` rows entirely: the
footer rendered a title with no body, and Skip described them as unreadable.
Both now group by what it costs the project — tables that are missing
(never created, or a migration that failed) against tables that could not be
verified — which is also what makes the sentences true: a failed row is
configured, so "this data table does not exist yet" was wrong about it.
Skip and the footer now read the same partition instead of each computing
one, so they cannot disagree again.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx
* docs: the SQL disclosure should not promise a run that cannot happen
An unknown row's only action re-reads the schema; nothing executes its SQL.
The summary still said "Show the SQL this will run", which is the sentence
the previous commit removed from the footer for the same reason. On those
rows it now says what the SQL is.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx
* fix: a row whose migrations are running does not offer "Set up"
Found by walking every branch on row.status rather than the ones I
remembered: `running` falls through to the catch-all action, which labelled
itself "Set up" in accent. Disabled, so nothing could come of it, but it is
the same label-outruns-state mistake the last rounds were spent on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx
* fix: never fill a path a resource of another type already holds
The presence probe matches on path, and a path says nothing about type. A
workspace resource of another kind sitting where the project wanted one of
ours was skipped as "already there", then read for missing fields against
the *project's* expected schema — so it looked like an empty stub, offered
Connect, and had its value replaced with credentials for a different
provider while keeping its own type. A working resource unrelated to the
import, destroyed.
Guarded at both ends. AppConnectInner checks the occupant's type before
updating, because `fillPath` only says "write into this path" and a caller
cannot be trusted to have checked. And the setup step records the conflict,
so the row explains that the project did not get the resource it shipped and
offers no action at all — every action there writes to that path.
Such a row is always listed, however full the occupant's value looks: it is
the only thing that tells the user something is missing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx
* fix: fail closed everywhere the import cannot tell
Codex was right that the occupant-type guard failed open: a getResource
that threw became `undefined`, which passed the mismatch test and left
filling enabled — so a transient read failure still overwrote the resource
the guard exists to protect. Only a read that succeeds and answers with
exactly this type now permits the write; a failed read, a missing type and
any other type all refuse.
That was the same "cannot tell, so proceed" this branch already fixed once
in settle(), so the rest of the wizard was swept for it. Two more:
findBlankResources dropped a row whenever getResource threw, on the
assumption that meant absent. Only a 404 means absent — and that failure the
import already reported. Any other error is a read that did not complete,
which says nothing about whether the credential needs filling; dropping the
row reports "all set" over one nobody filled. The row now stays and offers
no action, since none of them can be safe about a path this cannot read.
A resource type whose schema would not load left `required` empty, which
reads as "nothing missing" — so a half-filled resource passed as done. It
stays on the checklist; it just cannot name which fields are short.
The other four catches were checked and are already closed in the right
direction: probeWorkspace reports absent so the caller creates rather than
adopts, probeMigrationsApplied answers undefined which settles to a
non-actionable row, and afterWizard keeps whatever the run last said.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx
* fix: a refresh takes every field the fresh read decides
`refreshBlanks` merged only `missing` out of the new scan, so the two fields
added alongside it were left at whatever the row said before. Both reviewers
found the same seam from opposite ends: a resource that had just become
unreadable kept its old readable-looking row, and one that had come back
stayed blocked until a reload.
These fields describe what is at the path now, so the fresh read owns all of
them — and the branch that marks a row done clears them, because a row that
has left the blank list was read and is filled.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx
* fix: resolve the username in the destination, and lock a targeted name
Two P1s, both from the same earlier fix being half-done. Routing this
wizard's API calls through an explicit workspace left `$userStore` behind,
and that store describes the workspace the app is in. After a reload on
step 4 it names the workspace the user came from, so a resource path built
from it lands on `u/<someone-else>` inside the destination — failing an
ownership check, or for an admin, quietly putting database credentials in
another member's namespace. The membership is now resolved for the target
workspace, the way FolderPicker already did it.
And `initialName` was documented as "a starting point, not a lock" while
`onFinishAlso` targets that exact name. Renaming `main` to `other` created
`other`, ran the migrations against `main`, failed, and left a data table
nobody asked for. The field is locked when a caller passes follow-up work
bound to the name, and says why; without one it stays editable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx
* fix: resolve membership before seeding, and hold the name lock for the dialog
Two follow-ons to the previous commit, both where a value is read live that
should have been settled once.
The username was fetched in an effect while `open()` reset the wizard
immediately, so `defaultFolder()` ran against an empty username and seeded
`u/admin`. It was corrected only if `whoami` happened to win a race against
the folder list, and never if `whoami` failed — which is the case that
matters, since an admin would then save database credentials in another
member's namespace. `open()` now awaits the membership before reset, and a
destination whose membership cannot be read blocks setup outright rather
than guessing a path.
And the name lock read the live `initialName`, which is the caller's
`wizardFor` — cleared from `onDone`, which fires after a *failed* run too,
while the dialog stays up offering Back. The lock released exactly when the
user was most likely to edit the name, so the rename-then-retry path still
diverged from the migration target. It is captured at reset, for the life of
the dialog.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx
* fix: do not show the data table dialog before it knows the destination
`open()` became async so it could resolve the destination's membership
before seeding a resource path from it. But `openWizard` still set
`wizardOpen` first, and that is bound to the dialog's `opened` — so the
dialog was mounted, visible and clickable for the whole lookup, with the
username unresolved and `membershipFailed` not yet set. Setup reached in
that window writes exactly the wrong-namespace path the await was added to
prevent, and a late response could reset a dialog the user had already
touched or closed.
`open()` sets `opened` itself, once it has an answer. `wizardFor` alone
mounts the component, which is all `wizard?.open()` needs to exist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx
* fix: honour the configured base path, and guard a second Set up click
Windmill can be served under a prefix (`paths.base`, from VITE_BASE_URL),
and four import entrypoints compared or emitted `/projects/import` without
it. Under a base of `/windmill` the real pathname is
`/windmill/projects/import`, so the layout's picker exemption and both login
redirect checks stopped matching and sent people through the workspace
picker — and the compatibility redirect emitted a path outside the base
entirely, which is a 404. All four are now built from `base`.
`Login.svelte` takes it from `$lib/base` rather than `$app/paths` because it
already did; both read VITE_BASE_URL, and importing the second name into
that file collides with the first.
And the previous commit left Set up clickable while `open()` resolves the
destination membership, deliberately — but with no guard, a second click
starts a second lookup whose `reset()` lands on the dialog the first one
opened, wiping fields already filled. The action is disabled while a dialog
is opening.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg8vXUuHCH3aRkf91sfxjx
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
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.
description:Run the CI Codex PR review locally against this branch's unpushed work (committed + uncommitted) before pushing. Same policy and reasoning effort as the codex-pr-review GitHub action, on a newer model.
---
# Local Codex Review (pre-push)
@@ -11,17 +11,18 @@ 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:
- Model is `gpt-6-astra`; CI stays on `gpt-5.6-sol`. Not an oversight to reconcile: `gpt-6-astra` is confirmed on the ChatGPT auth `codex login` uses locally, while CI authenticates with `OPENAI_API_KEY` (`codex-pr-review.yml` prefers it over `CODEX_AUTH_JSON`) and that tier is unverified for the model. Move CI once API access is confirmed, or once CI switches to `CODEX_AUTH_JSON`.
- 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`.
-`codex` CLI **>= 0.153.4** installed and authed via `codex login`(an`OPENAI_API_KEY` in the environment takes priority and may not reach `gpt-6-astra` — see the model note above). Older CLIs reject the model with "requires a newer version of Codex"; `run.sh` checks the version up front. Upgrade with `npm install --global @openai/codex@0.153.4` (may need `sudo` for a global install). This matches the pin in `.github/workflows/codex-pr-review.yml` — the CLI version is the same on both sides, only the model differs.
-`git fetch` the base ref if it's stale, so the merge-base is accurate.
if echo "$CHANGED_FILES" | grep -qE '^(backend/windmill-git-sync/|backend/windmill-worker/src/result_processor\.rs|backend/windmill-api-workspaces/|backend/windmill-api-integration-tests/tests/git_sync|backend/windmill-common/src/workspaces\.rs|cli/src/commands/sync/|cli/src/utils/git\.ts|integration_tests/test/git_sync|\.github/workflows/git-sync-test\.yml)'; then
if echo "$CHANGED_FILES" | grep -qE '^(backend/windmill-git-sync/|backend/windmill-worker/src/result_processor\.rs|backend/windmill-api-workspaces/|backend/windmill-api-integration-tests/tests/git_sync|backend/windmill-common/src/workspaces\.rs|frontend/src/lib/hubPaths\.json|cli/src/commands/sync/|cli/src/utils/git\.ts|integration_tests/test/git_sync|\.github/workflows/git-sync-test\.yml)'; then
@@ -36,8 +36,8 @@ 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. 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`
- **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 and `xhigh` reasoning, on `gpt-6-astra` rather than the action's `gpt-5.6-sol`; requires the `codex` CLI >= 0.153.4.
- **Domain vocabulary**: `CONTEXT.md` — the words this codebase uses for its own concepts (step, step setting, trigger step, …). Name things the way it does.
- **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.
@@ -165,6 +165,15 @@ $NAV --root backend callees "X" # what does X call?
- Search for existing code to reuse before writing new code
- **A simpler design found late is still the design.** Work already spent is not an argument
for a shape, and neither is a clean review round, a passing suite, or a long PR thread. The
signal to stop and re-derive rather than patch again is a change that keeps growing to defend
its own structure: each review finding fixing an assumption the previous fix broke, the same
class of bug reappearing somewhere new, or most of the diff being consequences of one early
choice rather than the thing you set out to do. When that happens, say plainly what the
simpler design is and what switching costs — a migration, a review cycle restarted from zero,
work discarded — and let the user decide. Do not keep paying down the harder one because it
is nearly finished, and do not present the accumulated cost as a reason to continue.
- **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.
* make snowflake_oauth work as a dbt warehouse on every engine ([#11095](https://github.com/windmill-labs/windmill/issues/11095)) ([9fc50a2](https://github.com/windmill-labs/windmill/commit/9fc50a23fb75cb541c481247b7b25c206fb06d36))
### Bug Fixes
* accept any hub version of the git sync script in the token check ([#11099](https://github.com/windmill-labs/windmill/issues/11099)) ([670628b](https://github.com/windmill-labs/windmill/commit/670628b300ab119363adb5496ebfe3c6ccd80063))
* bring back Publish to Hub for scripts ([#11097](https://github.com/windmill-labs/windmill/issues/11097)) ([864e5f0](https://github.com/windmill-labs/windmill/commit/864e5f02ec1c2dd16c74524f551f44485df10a20))
* bundle deployed bun scripts whose only pin is on a dynamic import ([#11096](https://github.com/windmill-labs/windmill/issues/11096)) ([4afb9aa](https://github.com/windmill-labs/windmill/commit/4afb9aa677ac11d22e22e54bb6fd7881378b7005))
* clear a stale git auto-pull failure and show the status time ([#11100](https://github.com/windmill-labs/windmill/issues/11100)) ([e877b5f](https://github.com/windmill-labs/windmill/commit/e877b5f2e81b1741aee90e843c8cccc22f9eef24))
* stop a resource delete from taking variables it does not own ([#11102](https://github.com/windmill-labs/windmill/issues/11102)) ([2a21efa](https://github.com/windmill-labs/windmill/commit/2a21efa11b8307b331a8c20720028444dd3c62ff))
* **ai-sessions:** turn skills on by default, and group them by folder ([#11058](https://github.com/windmill-labs/windmill/issues/11058)) ([d8b9174](https://github.com/windmill-labs/windmill/commit/d8b9174235b97d0b4ef9f281e20e5704182d8dcb))
* background and wait_seconds for run_script, skip preprocessor ([#11092](https://github.com/windmill-labs/windmill/issues/11092)) ([2939c2d](https://github.com/windmill-labs/windmill/commit/2939c2dd4b129640d87ef45c7a24c6f215a3239a))
* give the chat the full MCP tool schema, and mark calls with the provider icon ([#11086](https://github.com/windmill-labs/windmill/issues/11086)) ([e7c6f85](https://github.com/windmill-labs/windmill/commit/e7c6f85553bd8efb0f7af0f488f615ac93c496ae))
* let apps hide the viewer login status on public urls ([#11089](https://github.com/windmill-labs/windmill/issues/11089)) ([6056ec7](https://github.com/windmill-labs/windmill/commit/6056ec7148bce9f8ed171dd29f544696c335de7d))
* remove the viewer login status badge from public apps ([#11090](https://github.com/windmill-labs/windmill/issues/11090)) ([75d7bee](https://github.com/windmill-labs/windmill/commit/75d7bee178886461fe49090d606a708f25c02d1a))
* run a deployed flow through the chat's argument form ([#11085](https://github.com/windmill-labs/windmill/issues/11085)) ([b50de89](https://github.com/windmill-labs/windmill/commit/b50de8947908f1a5a4e9472afe6c0ecd25892e93))
* run a flow test through the chat's argument form ([#11069](https://github.com/windmill-labs/windmill/issues/11069)) ([172d6c2](https://github.com/windmill-labs/windmill/commit/172d6c275b92b39d13848b543df9db10148e94ef))
### Bug Fixes
* attach TLS to gRPC OTLP exporters for https endpoints ([#11078](https://github.com/windmill-labs/windmill/issues/11078)) ([f915ed6](https://github.com/windmill-labs/windmill/commit/f915ed6a46e14341ddb213e869090edb68763032))
* keep pinned import versions of imported scripts in bun lockfiles ([#11082](https://github.com/windmill-labs/windmill/issues/11082)) ([57f8b08](https://github.com/windmill-labs/windmill/commit/57f8b0826ad61cb118d0cafbbc9203327d858940))
* let admins and background sync reach private git hosts ([#11084](https://github.com/windmill-labs/windmill/issues/11084)) ([fa53099](https://github.com/windmill-labs/windmill/commit/fa53099e2b87a8676c5d6a18e77844e17ff45efd))
* serve instance env settings at the documented /settings/local path ([#11075](https://github.com/windmill-labs/windmill/issues/11075)) ([f8f7c00](https://github.com/windmill-labs/windmill/commit/f8f7c0009f32c1440566420725b12e78ca804b03))
* show symlinked files in the git repo viewer ([#11081](https://github.com/windmill-labs/windmill/issues/11081)) ([e6d4f44](https://github.com/windmill-labs/windmill/commit/e6d4f44a6122dab47fbee2a2a2b4330a62841bed))
* support gzip and zstd compression for OTLP export over gRPC ([#11077](https://github.com/windmill-labs/windmill/issues/11077)) ([b156778](https://github.com/windmill-labs/windmill/commit/b156778da24e3827f723e503f80df68de87ddf7e))
* unpin only the specifiers in the bundle a bun modules run executes ([#11083](https://github.com/windmill-labs/windmill/issues/11083)) ([30ffdbe](https://github.com/windmill-labs/windmill/commit/30ffdbecc15270562ceed3030c50a1cf81b1195c))
### Performance Improvements
* lazy-load the low-code runtime on public app pages ([#11087](https://github.com/windmill-labs/windmill/issues/11087)) ([e651b4c](https://github.com/windmill-labs/windmill/commit/e651b4cd63c3bd64a8739c9f60c6dfa437f19b4b))
* add a minimal skin for the approval page and slack/teams ([#11061](https://github.com/windmill-labs/windmill/issues/11061)) ([63cb46d](https://github.com/windmill-labs/windmill/commit/63cb46d7bb9db1d996aa38e06bd3afebc60111bd))
* live queue status per tag and bounded queue metric charts ([#11067](https://github.com/windmill-labs/windmill/issues/11067)) ([569adb8](https://github.com/windmill-labs/windmill/commit/569adb85c1885d289e80a70a166f0f74e6d5ba83))
* **otel:** read the OTLP metrics temporality preference ([#11064](https://github.com/windmill-labs/windmill/issues/11064)) ([2f88769](https://github.com/windmill-labs/windmill/commit/2f8876908719b3640d7cfc9364a7b0f7145fc356))
* **otel:** support standard OTEL resource attribute env vars ([#10974](https://github.com/windmill-labs/windmill/issues/10974)) ([0a40eea](https://github.com/windmill-labs/windmill/commit/0a40eea37a7dbde5fc4760d6333d81186dfee255))
* report script metadata with no content file in wmill lint ([#11053](https://github.com/windmill-labs/windmill/issues/11053)) ([8820b9f](https://github.com/windmill-labs/windmill/commit/8820b9fc644c6620c50517cd0d902015e2e670e2))
* show the workspace an operator is in, and let them switch ([#11059](https://github.com/windmill-labs/windmill/issues/11059)) ([385086f](https://github.com/windmill-labs/windmill/commit/385086ffc21c72cd07624584932a4a301c23a732))
* tuck other users' spaces into a collapsible home tree row ([#11073](https://github.com/windmill-labs/windmill/issues/11073)) ([d87f089](https://github.com/windmill-labs/windmill/commit/d87f089288996af9ea7e3b017a5ef35d4ded880d))
### Bug Fixes
* **ai-chat:** test_run_flow could test a different flow than the one asked ([#11066](https://github.com/windmill-labs/windmill/issues/11066)) ([fa73539](https://github.com/windmill-labs/windmill/commit/fa73539839071491fb2cbad9242f52ad22b975bf))
* bound list_jobs runtime and paginate runs on the sorted column ([#11072](https://github.com/windmill-labs/windmill/issues/11072)) ([f517402](https://github.com/windmill-labs/windmill/commit/f51740253871960b55ab2aa8989e8df3fbde0351))
* **frontend:** clear the flow graph selection through xyflow's store ([#11056](https://github.com/windmill-labs/windmill/issues/11056)) ([b4be8bc](https://github.com/windmill-labs/windmill/commit/b4be8bc5354fbd3a47c267c331aaf603c0f90e6e))
* **frontend:** recompute dataflow edges when selecting a step ([#11070](https://github.com/windmill-labs/windmill/issues/11070)) ([08d876a](https://github.com/windmill-labs/windmill/commit/08d876aebf32ebb995a8c1839aa87794c0176bed))
* **frontend:** restore heading sizes in note markdown and keep group notes on id change ([#11047](https://github.com/windmill-labs/windmill/issues/11047)) ([e63072c](https://github.com/windmill-labs/windmill/commit/e63072c216383700a23504be89782f4c69657174))
* give every table a primary key so the db can be logically replicated ([#11036](https://github.com/windmill-labs/windmill/issues/11036)) ([e62bfdc](https://github.com/windmill-labs/windmill/commit/e62bfdcd8c6f1389601ccd2b5809c5eff0ff262e))
* keep an app's deployed policy on wmill push ([#11049](https://github.com/windmill-labs/windmill/issues/11049)) ([0af7675](https://github.com/windmill-labs/windmill/commit/0af7675588300863883a99c7f14bc53cafec8a7e))
* refuse cross-site GET requests that run Hub scripts ([#11054](https://github.com/windmill-labs/windmill/issues/11054)) ([ab9efc8](https://github.com/windmill-labs/windmill/commit/ab9efc897cc94d61a67263772806b15e9225cece))
* skip the deploy PR when the git sync push committed nothing ([#11076](https://github.com/windmill-labs/windmill/issues/11076)) ([8ecbd33](https://github.com/windmill-labs/windmill/commit/8ecbd339eef7314a93d599fcea4377299d4c493d))
* space the trailing AI settings cards ([#11044](https://github.com/windmill-labs/windmill/issues/11044)) ([5d7eed1](https://github.com/windmill-labs/windmill/commit/5d7eed1c02b0966289cc8cca00a15d76dad187a5))
* surface why a private or untrusted git host is unreachable ([#11068](https://github.com/windmill-labs/windmill/issues/11068)) ([c57b18e](https://github.com/windmill-labs/windmill/commit/c57b18e46fcdd319213fe0a537cb18de418ca688))
### Performance Improvements
* index the FK columns that cascade on workspace delete ([#11052](https://github.com/windmill-labs/windmill/issues/11052)) ([9a563f6](https://github.com/windmill-labs/windmill/commit/9a563f6d72da28fe09b785cd0683e9698df72bba))
* only write queue metrics when a tag's backlog changes ([#11055](https://github.com/windmill-labs/windmill/issues/11055)) ([9d75929](https://github.com/windmill-labs/windmill/commit/9d75929247ea2ec39286971fcbebf95d886194f3))
* run and test scripts from the AI chat through an argument form ([#11001](https://github.com/windmill-labs/windmill/issues/11001)) ([a6abf2c](https://github.com/windmill-labs/windmill/commit/a6abf2c8a744e9ee6acf4830cdfbb84f2f95cb36))
* add a dismissible instance-wide announcement banner ([#11037](https://github.com/windmill-labs/windmill/issues/11037)) ([abf4c6c](https://github.com/windmill-labs/windmill/commit/abf4c6c2348014ea4401b9be62b5b15e5800e879))
* batch chained DDL statements into a single migration ([#11038](https://github.com/windmill-labs/windmill/issues/11038)) ([656e609](https://github.com/windmill-labs/windmill/commit/656e609595833bc854f845d35cf43157e76d732f))
* create the cloud workspace in onboarding, and teach the empty home ([#10959](https://github.com/windmill-labs/windmill/issues/10959)) ([fd35b47](https://github.com/windmill-labs/windmill/commit/fd35b4765843879cb2254f402c142fd7510f1916))
* link from the public run view to the authenticated run page ([#11041](https://github.com/windmill-labs/windmill/issues/11041)) ([09b81a9](https://github.com/windmill-labs/windmill/commit/09b81a9294bed2795a1a7b688d0b02e0a61957ea))
* make guest access unavailable on the shared cloud ([#11040](https://github.com/windmill-labs/windmill/issues/11040)) ([0b63e0a](https://github.com/windmill-labs/windmill/commit/0b63e0a6929088ff25def4fd6547cf61668251a5))
### Bug Fixes
* ignore comments and continuations in python lockfiles ([#11035](https://github.com/windmill-labs/windmill/issues/11035)) ([90c4e10](https://github.com/windmill-labs/windmill/commit/90c4e1020a2ff896977648dd68b572413cea7709))
* refetch an unparseable hub script cache entry instead of panicking ([#11033](https://github.com/windmill-labs/windmill/issues/11033)) ([88c3ebd](https://github.com/windmill-labs/windmill/commit/88c3ebdfc1325ffbea4521d854e71d231a6409c4))
* stop a new AI session adopting a legacy sidebar chat ([#11039](https://github.com/windmill-labs/windmill/issues/11039)) ([1076b63](https://github.com/windmill-labs/windmill/commit/1076b638d987ba99c5c27e5478ca580534b9d572))
* bring gitlab repositories to parity for git sync ([#10938](https://github.com/windmill-labs/windmill/issues/10938)) ([9444049](https://github.com/windmill-labs/windmill/commit/9444049d6013c77a5f25f01a736eb5cd741fb3e6))
* draw a dbt column trace, across projects and the pipeline boundary ([#11014](https://github.com/windmill-labs/windmill/issues/11014)) ([33f9828](https://github.com/windmill-labs/windmill/commit/33f9828c3ed15fe63fccedc1550584f15c0490ab))
* durable dbt state per environment, and `--defer` onto it ([#10975](https://github.com/windmill-labs/windmill/issues/10975)) ([621fac5](https://github.com/windmill-labs/windmill/commit/621fac55abcd1859e8c8c06e5f4412e61bb85d59))
* ingest dbt column lineage and real column schemas from the engine's parquet index ([#10977](https://github.com/windmill-labs/windmill/issues/10977)) ([0139467](https://github.com/windmill-labs/windmill/commit/0139467b01b82e4b3d474ca3f205358fa607d19a))
* let a worker group override the dependency cache object store ([#11019](https://github.com/windmill-labs/windmill/issues/11019)) ([de98adf](https://github.com/windmill-labs/windmill/commit/de98adf055835ab7c4d6305e7d5d3bdab915876b))
* **nativets:** bound fetch on a peer that never answers ([#11026](https://github.com/windmill-labs/windmill/issues/11026)) ([785277e](https://github.com/windmill-labs/windmill/commit/785277e0bb2ea77b71a89dd4d389d439dfcf9e03))
* recognize `// volume:` mounts in PHP scripts ([#11018](https://github.com/windmill-labs/windmill/issues/11018)) ([f081fb1](https://github.com/windmill-labs/windmill/commit/f081fb10705cadf99e99dfa786d1cc2ebf0447db))
* report a WAC task failure the workflow body never awaited ([#11017](https://github.com/windmill-labs/windmill/issues/11017)) ([3e3a41d](https://github.com/windmill-labs/windmill/commit/3e3a41d418d4ee3fe060bd3acf3324f311d89c8a))
* retry a workflow-as-code task from its task options ([#11013](https://github.com/windmill-labs/windmill/issues/11013)) ([d3f305d](https://github.com/windmill-labs/windmill/commit/d3f305db982b7c5dc49babf9bec8b62adcd2557d))
### Bug Fixes
* chain redeploys onto a retired path's version history ([#11029](https://github.com/windmill-labs/windmill/issues/11029)) ([0b37226](https://github.com/windmill-labs/windmill/commit/0b372260787edb7e9627ad4fb6637ad5f1024e0a))
* make the native trigger disable/enable toggle actually save ([#11024](https://github.com/windmill-labs/windmill/issues/11024)) ([448fce9](https://github.com/windmill-labs/windmill/commit/448fce93f743d5b2ef2a2d4496eb2ec238594a0a))
* offload php signature parsing from async workers ([#11027](https://github.com/windmill-labs/windmill/issues/11027)) ([2cb02e3](https://github.com/windmill-labs/windmill/commit/2cb02e3b3398db49f16377dd79dde2dd6fb5cc02))
* reduce php parser stack use in debug workers ([#11025](https://github.com/windmill-labs/windmill/issues/11025)) ([2ae8509](https://github.com/windmill-labs/windmill/commit/2ae8509b14f112c9ef5b71321e8fd52596a16c10))
* **git-sync:** sync extra_perms for variables ([#11004](https://github.com/windmill-labs/windmill/issues/11004)) ([ee9e550](https://github.com/windmill-labs/windmill/commit/ee9e550a484fda286eeab43b7db5f314b8b2d0d9))
* go to referenced row from foreign-keyed cells in the database manager ([#10998](https://github.com/windmill-labs/windmill/issues/10998)) ([e2b63d1](https://github.com/windmill-labs/windmill/commit/e2b63d177ae4e5c980cb5da34154540c90771b63))
* let `// materialize` declare a `dbt://` warehouse-relation write ([#10978](https://github.com/windmill-labs/windmill/issues/10978)) ([c6e0302](https://github.com/windmill-labs/windmill/commit/c6e0302d7c1c60147f19d55a3923be8b1aa99c9c))
* report resource type picks to the hub and rank pickers by popularity ([#10982](https://github.com/windmill-labs/windmill/issues/10982)) ([48a5615](https://github.com/windmill-labs/windmill/commit/48a56158c135c3b13a02f73b7b8438bc691f85b4))
* run a linked AI agent's draft when testing a flow, and offer to deploy it ([#10993](https://github.com/windmill-labs/windmill/issues/10993)) ([7feaf61](https://github.com/windmill-labs/windmill/commit/7feaf619cf0ec021d66be14ef535cf2149bec58a))
* show the new-tab icon on a chat path pill while the modifier is held ([#10976](https://github.com/windmill-labs/windmill/issues/10976)) ([5da4ea4](https://github.com/windmill-labs/windmill/commit/5da4ea43fbd01e43aa14e75dc597d7ce5d8797ab))
### Bug Fixes
* **cli:** keep permissioned_as on single-item push, as sync push does ([#11000](https://github.com/windmill-labs/windmill/issues/11000)) ([5f3f99b](https://github.com/windmill-labs/windmill/commit/5f3f99ba6915b7c5df663a30b35f4cd02050e728))
* **cli:** say which workspace id is targeted, and when wmill.yaml is bypassed ([#11006](https://github.com/windmill-labs/windmill/issues/11006)) ([7643e9b](https://github.com/windmill-labs/windmill/commit/7643e9bd77c56f72596b8dca50801baf58984198))
* **frontend:** no phantom draft when opening a CLI-pushed script ([#10997](https://github.com/windmill-labs/windmill/issues/10997)) ([1be390a](https://github.com/windmill-labs/windmill/commit/1be390aa878e15a58f530f3a878e8f9caeb89c43))
* **frontend:** stop hover flicker on asset nodes shared with an overflow popover ([#10996](https://github.com/windmill-labs/windmill/issues/10996)) ([519a5c8](https://github.com/windmill-labs/windmill/commit/519a5c8bc70b44a7417e83c26c7b9c58b2c4fb9c))
* let a draft-only schedule, trigger or resource be deleted ([#11010](https://github.com/windmill-labs/windmill/issues/11010)) ([8d0f475](https://github.com/windmill-labs/windmill/commit/8d0f4754e4e0c78696ee0c97ff2de3016ece3bac))
* point the app viewer's edit button at the editor for the app's kind ([#11009](https://github.com/windmill-labs/windmill/issues/11009)) ([8f553ea](https://github.com/windmill-labs/windmill/commit/8f553eab353103fd8a28a00532e1766f133590de))
* seed runs page filter defaults through the url so they survive sync ([#11005](https://github.com/windmill-labs/windmill/issues/11005)) ([f381acd](https://github.com/windmill-labs/windmill/commit/f381acdb37f66f5e272bc37938e69f734987d53f))
* stop an untouched item's form from saving a draft nobody wrote ([#10964](https://github.com/windmill-labs/windmill/issues/10964)) ([c3f7f8a](https://github.com/windmill-labs/windmill/commit/c3f7f8a45830fb548aa628ebf6e2b6c95c6de67f))
* write and read python job files as utf-8, not the platform locale ([#10994](https://github.com/windmill-labs/windmill/issues/10994)) ([670404f](https://github.com/windmill-labs/windmill/commit/670404ffe27fedc3858b46b0c6b3312fbe175e13))
* **ai-sessions:** replace the context panel with an assistant settings modal ([#10919](https://github.com/windmill-labs/windmill/issues/10919)) ([fda7b3f](https://github.com/windmill-labs/windmill/commit/fda7b3f086619e3716e5894c07be127104174f1d))
* **frontend:** group the agent form and edit saved agents as drafts ([#10880](https://github.com/windmill-labs/windmill/issues/10880)) ([f037c73](https://github.com/windmill-labs/windmill/commit/f037c73d104fffe7bb2640a5b1f2a92154c85e06))
* guest app execution mode, a role that takes no seat ([#10929](https://github.com/windmill-labs/windmill/issues/10929)) ([fce635d](https://github.com/windmill-labs/windmill/commit/fce635d3c4c8962f448140ceb55a00fb99012701))
* guest JWT entry for embedded apps ([#10954](https://github.com/windmill-labs/windmill/issues/10954)) ([8aab503](https://github.com/windmill-labs/windmill/commit/8aab5034a68a4aafb264b0e86d000ef58f4a8511))
* instrument sandbox isolation, data tables and in-flow script edits ([#10981](https://github.com/windmill-labs/windmill/issues/10981)) ([130a2f7](https://github.com/windmill-labs/windmill/commit/130a2f74083ba1bd308beeb86e2cbbaa41fd3345))
* make S3 permission rules reorderable by drag and drop ([#10958](https://github.com/windmill-labs/windmill/issues/10958)) ([2257b05](https://github.com/windmill-labs/windmill/commit/2257b05b2857c7ae2b5ae0b4f9004e2d4e757925))
* reconcile IdP instance groups from the SSO groups claim ([#10957](https://github.com/windmill-labs/windmill/issues/10957)) ([79426a1](https://github.com/windmill-labs/windmill/commit/79426a1a68a6b19e12af4633b8a79d07a103a106))
### Bug Fixes
* deploy a relocked script version only when its lock changed ([#10966](https://github.com/windmill-labs/windmill/issues/10966)) ([1113828](https://github.com/windmill-labs/windmill/commit/11138284acc4c1d8673e86823c7f74c9e1f419e6))
* **frontend:** render ordered lists in markdown descriptions ([#10973](https://github.com/windmill-labs/windmill/issues/10973)) ([a0295b2](https://github.com/windmill-labs/windmill/commit/a0295b20c436fd3f2bd6a6d294ae3cee005391e8))
* keep braces inside string tool arguments out of JSON depth count ([#10965](https://github.com/windmill-labs/windmill/issues/10965)) ([3e3d2a6](https://github.com/windmill-labs/windmill/commit/3e3d2a636334146014926841949372083e6e8516))
* keep the instance user editor popover inside the viewport ([#10979](https://github.com/windmill-labs/windmill/issues/10979)) ([1901d31](https://github.com/windmill-labs/windmill/commit/1901d3193bfc6a9e29d0b7c5389fef44ff9d3687))
* meter WAC compute per segment, not the whole sleep ([#10985](https://github.com/windmill-labs/windmill/issues/10985)) ([5428710](https://github.com/windmill-labs/windmill/commit/54287102b22dd17903cdd4b48c5828875e5b9be4))
* name the extension to load when duckdb autoload hits the fence ([#10972](https://github.com/windmill-labs/windmill/issues/10972)) ([64b6798](https://github.com/windmill-labs/windmill/commit/64b679879936e2ddf4dbc2f90edbd56e3893bd83))
* **oauth:** show the account chooser on an explicit Google/Microsoft login ([#10961](https://github.com/windmill-labs/windmill/issues/10961)) ([9f7908e](https://github.com/windmill-labs/windmill/commit/9f7908e2622647388768b574083cc48a6e1990f1))
* patch critical CVEs in the worker image ([#10962](https://github.com/windmill-labs/windmill/issues/10962)) ([b100606](https://github.com/windmill-labs/windmill/commit/b100606da6a61f2dbcb24516363f43643bc917e3))
* render the MCP OAuth consent page without a workspace ([#10988](https://github.com/windmill-labs/windmill/issues/10988)) ([ebfac29](https://github.com/windmill-labs/windmill/commit/ebfac29096f12c4da2df45d5d82db83d352f3426))
* stand the WAC park down for a cancel that beat it to the row ([#10990](https://github.com/windmill-labs/windmill/issues/10990)) ([f977f5b](https://github.com/windmill-labs/windmill/commit/f977f5bf8b1ac70d3afbdc8ad6fcbe072cc51ebc))
* expose request headers to scripts invoked via MCP ([#10903](https://github.com/windmill-labs/windmill/issues/10903)) ([e474e88](https://github.com/windmill-labs/windmill/commit/e474e8803ce2ff5c2df09a58dab51d45f5c922ca))
* reuse an existing workspace resource in the project import wizard ([#10935](https://github.com/windmill-labs/windmill/issues/10935)) ([582761e](https://github.com/windmill-labs/windmill/commit/582761e37c776e92dc1c6ebfee8c4efe7c35d822))
### Bug Fixes
* bump git sync hub scripts to cli 1.802.1, test the fork ui pull ([#10955](https://github.com/windmill-labs/windmill/issues/10955)) ([ca88009](https://github.com/windmill-labs/windmill/commit/ca8800959aa6a0017cc29bad187c9f49e0d13cc4))
* **cli:** make a sync push into a fork converge on schedules and inline names ([#10951](https://github.com/windmill-labs/windmill/issues/10951)) ([0f5a1db](https://github.com/windmill-labs/windmill/commit/0f5a1db2abba8df30a2f975f4498e269f13cf93d))
* fade the home Build with AI placeholder every 10s instead of typing it ([#10953](https://github.com/windmill-labs/windmill/issues/10953)) ([3d089b5](https://github.com/windmill-labs/windmill/commit/3d089b57344f5814086e6176301c5031dc519674))
* let operators use wmill.datatable() from within running jobs ([#10931](https://github.com/windmill-labs/windmill/issues/10931)) ([9b64a89](https://github.com/windmill-labs/windmill/commit/9b64a89cd46ae718d6c58fa12f925fa041fb1032))
* add retention cleanup for the otel_traces table ([#10949](https://github.com/windmill-labs/windmill/issues/10949)) ([d472193](https://github.com/windmill-labs/windmill/commit/d472193e5bf5f6428e0096a402eb2c9299634fb2))
* open path links from chat messages in the session preview panel ([#10881](https://github.com/windmill-labs/windmill/issues/10881)) ([f10ac6c](https://github.com/windmill-labs/windmill/commit/f10ac6c2b3644fb16697e650efbc4f7cd3c6944c))
* restore owner and label filter chips on the homepage ([#10942](https://github.com/windmill-labs/windmill/issues/10942)) ([ccf8476](https://github.com/windmill-labs/windmill/commit/ccf84761dd664b9228dfe2f65867e8c32cd20c21))
* **sessions:** offer the item you came from when starting a new session ([#10940](https://github.com/windmill-labs/windmill/issues/10940)) ([d3747d6](https://github.com/windmill-labs/windmill/commit/d3747d62555ebcb09c78cfabcaa3b6177758d6ea))
* workspace setting to hide the AI assistant, agent steps unaffected ([#10941](https://github.com/windmill-labs/windmill/issues/10941)) ([fdd3b36](https://github.com/windmill-labs/windmill/commit/fdd3b36423344a2e1a464674179406581074e926))
### Bug Fixes
* apply object-storage test SSRF validation to all non-super-admins ([#10933](https://github.com/windmill-labs/windmill/issues/10933)) ([4fef119](https://github.com/windmill-labs/windmill/commit/4fef1195adaa9fa036a219884bd6c996460ca37f))
* connect to dev server instead of localhost ([#10912](https://github.com/windmill-labs/windmill/issues/10912)) ([337154b](https://github.com/windmill-labs/windmill/commit/337154b8304a5969f35216add627b5c1153c0f6c))
* preselect first row of AI agent and AI sandbox insert panes ([#10937](https://github.com/windmill-labs/windmill/issues/10937)) ([95b6bbd](https://github.com/windmill-labs/windmill/commit/95b6bbd46ada11d96a914ae5b0e92aba4dd02530))
* record supplied script lock hashes so importers can skip relocking ([#10915](https://github.com/windmill-labs/windmill/issues/10915)) ([17ba521](https://github.com/windmill-labs/windmill/commit/17ba521c352aec65a8270893752bbadd7f3d6eaa))
* sandbox script-controlled content types in result_to_response ([#10932](https://github.com/windmill-labs/windmill/issues/10932)) ([419741e](https://github.com/windmill-labs/windmill/commit/419741e5d226c67c51429094fb6ded9474afed99))
* **ai-chat:** make reusable skills ai_skill resources you select per workspace ([#10914](https://github.com/windmill-labs/windmill/issues/10914)) ([cfcfe29](https://github.com/windmill-labs/windmill/commit/cfcfe298dd9ab50196bd64926ef78c4563f58c2c))
* **ai-sessions:** show a running session across tabs and reload finished turns ([#10916](https://github.com/windmill-labs/windmill/issues/10916)) ([816dc9d](https://github.com/windmill-labs/windmill/commit/816dc9dcd2c310e499d2d210a0abcd403469f29c))
* edit folders and groups in a drawer that saves once ([#10873](https://github.com/windmill-labs/windmill/issues/10873)) ([5d5ad4e](https://github.com/windmill-labs/windmill/commit/5d5ad4e8974e076ef53a26a5584e4209255a2248))
* make the home Build with AI composer dismissible, quiet the rest of the home page ([#10930](https://github.com/windmill-labs/windmill/issues/10930)) ([772fafe](https://github.com/windmill-labs/windmill/commit/772fafec8316a1e0c0e76b9a0737cc41d40a9a8c))
### Bug Fixes
* let a principal without a login account own a draft ([#10925](https://github.com/windmill-labs/windmill/issues/10925)) ([94af8d0](https://github.com/windmill-labs/windmill/commit/94af8d0fb5aceebe83936fd6761c6c1c02c75323))
* resolve chat path links against the session's operating workspace ([#10924](https://github.com/windmill-labs/windmill/issues/10924)) ([9074de2](https://github.com/windmill-labs/windmill/commit/9074de25ea730ca02653c9a2e2b8b99eda6f3137))
* add top margin to the home Build with AI section ([#10909](https://github.com/windmill-labs/windmill/issues/10909)) ([bedf5ae](https://github.com/windmill-labs/windmill/commit/bedf5ae57445025729e94e16f1b5f13f6ff38ffa))
* **ai-chat:** consume an @ mention with the message that carried it ([#10907](https://github.com/windmill-labs/windmill/issues/10907)) ([c512110](https://github.com/windmill-labs/windmill/commit/c512110a1f8d0d3437c20048f6446ef62b10222c))
* keep a local dbt descriptor under sync pull --keep-deleted ([#10911](https://github.com/windmill-labs/windmill/issues/10911)) ([4b5be38](https://github.com/windmill-labs/windmill/commit/4b5be386ce0f851a087f43c0b0ac6e4b1b055a47))
* keep windmill-indexer out of builds without tantivy ([#10908](https://github.com/windmill-labs/windmill/issues/10908)) ([db0f004](https://github.com/windmill-labs/windmill/commit/db0f004613e3f90428fea4c824cc53f1b2fc03b0))
* add --keep-deleted flag to wmill sync pull and push ([#10878](https://github.com/windmill-labs/windmill/issues/10878)) ([66123f3](https://github.com/windmill-labs/windmill/commit/66123f3a9b8978c0084b02f50b44cffba125a13a))
* day-partition the service log index and expire whole chunks ([#10893](https://github.com/windmill-labs/windmill/issues/10893)) ([d91ee46](https://github.com/windmill-labs/windmill/commit/d91ee4614a70f20a194f47e190327129f499ec63))
* free AI tokens + home search/filter revamp ([#10020](https://github.com/windmill-labs/windmill/issues/10020)) ([716ce2e](https://github.com/windmill-labs/windmill/commit/716ce2ece00cd5cfb8641afc6432636bc4aa46e9))
* make the service log retention period an instance setting ([#10889](https://github.com/windmill-labs/windmill/issues/10889)) ([815de49](https://github.com/windmill-labs/windmill/commit/815de49e2322f85ca92b1e41a2bcd22591ebe93f))
* rework the evals dataset drawer and run navigation ([#10884](https://github.com/windmill-labs/windmill/issues/10884)) ([1462f17](https://github.com/windmill-labs/windmill/commit/1462f17643302127b4bd76bacfde80cc03f9d606))
* serve service log context from parquet and retire the raw log files ([#10892](https://github.com/windmill-labs/windmill/issues/10892)) ([338d75c](https://github.com/windmill-labs/windmill/commit/338d75cc5227e352cb84828c99bfd3b984cf0fa5))
* serve service log retrieval from a columnar parquet store ([#10886](https://github.com/windmill-labs/windmill/issues/10886)) ([7c1a785](https://github.com/windmill-labs/windmill/commit/7c1a785f756ed27e4425f6534709b19971a73a97))
### Bug Fixes
* correct the service log ingest flush boundary ([#10898](https://github.com/windmill-labs/windmill/issues/10898)) ([ac56586](https://github.com/windmill-labs/windmill/commit/ac56586c0e56d4022761d3c80306a03d57f8bfcb))
* harden the service log indexer's recovery and read paths ([#10904](https://github.com/windmill-labs/windmill/issues/10904)) ([831370c](https://github.com/windmill-labs/windmill/commit/831370cdde8e06f4298b17baa1a0041bacdd98c7))
* keep raw-app editor selection consistent across sidebar and tabs ([#10885](https://github.com/windmill-labs/windmill/issues/10885)) ([b57e231](https://github.com/windmill-labs/windmill/commit/b57e231c2bf5e5fe007f0aa7b958a51e32b47141))
* register every rotated service log file exactly once ([#10891](https://github.com/windmill-labs/windmill/issues/10891)) ([c817248](https://github.com/windmill-labs/windmill/commit/c8172480b0b1be6c57210212afc71d6ec8711235))
* show a loading indicator while the initial data table migration is generated ([#10900](https://github.com/windmill-labs/windmill/issues/10900)) ([b998267](https://github.com/windmill-labs/windmill/commit/b998267c91b9dcf02787768f6205cc5aeda494fb))
* track outstanding service log files on the rows themselves ([#10894](https://github.com/windmill-labs/windmill/issues/10894)) ([aa4a6ff](https://github.com/windmill-labs/windmill/commit/aa4a6ffd66813010a79c07741b01a984ed4e7df6))
### Performance Improvements
* add service log documents to the index one batch at a time ([#10906](https://github.com/windmill-labs/windmill/issues/10906)) ([0c2eb0a](https://github.com/windmill-labs/windmill/commit/0c2eb0ae3d18f49c21370131d15011e8dd103746))
* enable Anthropic prompt caching on Vertex AI agent steps ([#10876](https://github.com/windmill-labs/windmill/issues/10876)) ([320f400](https://github.com/windmill-labs/windmill/commit/320f4005124202852e6e9c70b394e7f87231d278))
* instrument AI fill/fix, evals, agents and the debugger ([#10853](https://github.com/windmill-labs/windmill/issues/10853)) ([0bbd559](https://github.com/windmill-labs/windmill/commit/0bbd559ac8a35dba04ba5e8d6f2fd8d1d1124891))
### Bug Fixes
* **datatables:** stop a fork's pg_dump restore from failing silently ([#10830](https://github.com/windmill-labs/windmill/issues/10830)) ([3ce9bbc](https://github.com/windmill-labs/windmill/commit/3ce9bbc7168b837cb2111aabd533bb67803502b8))
* key build artifact caches on a runnable's inline modules ([#10819](https://github.com/windmill-labs/windmill/issues/10819)) ([b72ccc3](https://github.com/windmill-labs/windmill/commit/b72ccc35934165b4bad112b947ca5af064aab26f))
* nested template literals in step inputs, and unresolvable $args tags ([#10856](https://github.com/windmill-labs/windmill/issues/10856)) ([8f349c0](https://github.com/windmill-labs/windmill/commit/8f349c032a0d75fc3350292075e5050a030f6166))
* pre-fill the test panel JSON args editor and align its placeholder ([#10871](https://github.com/windmill-labs/windmill/issues/10871)) ([fb82f36](https://github.com/windmill-labs/windmill/commit/fb82f36e6d6492dd0740984d8d78ea4eaa30361e))
* reject a prefixed error_handler_path on triggers ([#10847](https://github.com/windmill-labs/windmill/issues/10847)) ([d334831](https://github.com/windmill-labs/windmill/commit/d33483173526a3b352d2829ac8a2e1e229cc1127))
* a wizard for importing a hub project, and finishing what the import cannot ([#10729](https://github.com/windmill-labs/windmill/issues/10729)) ([2913339](https://github.com/windmill-labs/windmill/commit/29133398f99cd2dd5b33057ee9df4492d82e067a))
* configurable expiry for presigned s3 public url signatures ([#10835](https://github.com/windmill-labs/windmill/issues/10835)) ([8a6dc27](https://github.com/windmill-labs/windmill/commit/8a6dc27236aca67f0efe941d9606b787c2305ea8))
* **frontend:** flag the fork-compare datatable schema diff as legacy ([#10829](https://github.com/windmill-labs/windmill/issues/10829)) ([07c77ea](https://github.com/windmill-labs/windmill/commit/07c77ead7425f1877372d358d867445a4c525c96))
* keep a Hub project live while an update is under review ([#10814](https://github.com/windmill-labs/windmill/issues/10814)) ([c04b570](https://github.com/windmill-labs/windmill/commit/c04b5705745c36ecbb3a551ac59459218d2e3807))
### Bug Fixes
* **cli:** keep svelte component styles in the raw-app bundle ([#10838](https://github.com/windmill-labs/windmill/issues/10838)) ([b8bf539](https://github.com/windmill-labs/windmill/commit/b8bf539c3fe2b4db9c74dd73f04b3029287acdc6))
* **debugger:** parse bun 1.4's UUID inspector token ([#10828](https://github.com/windmill-labs/windmill/issues/10828)) ([4658224](https://github.com/windmill-labs/windmill/commit/46582245926a7f8ea961bcd125a58fbfba3530cf))
* force HTTP router rebuild on trigger-change notification ([#10849](https://github.com/windmill-labs/windmill/issues/10849)) ([ffdf17e](https://github.com/windmill-labs/windmill/commit/ffdf17ef8dc5575dd92d62d0d0ba887c1e378576))
* **frontend:** follow the operating workspace in step input forms ([#10834](https://github.com/windmill-labs/windmill/issues/10834)) ([6b73145](https://github.com/windmill-labs/windmill/commit/6b73145e7220232601538b801ebc9dc73fe79bbb))
* **frontend:** key the GitHub App installation selector on installation_id ([#10831](https://github.com/windmill-labs/windmill/issues/10831)) ([78331fd](https://github.com/windmill-labs/windmill/commit/78331fda8b290a2d9a5dd92b8362ff32c8b39432))
* **frontend:** operator menu opens on hover, pins on click ([#10824](https://github.com/windmill-labs/windmill/issues/10824)) ([665f83e](https://github.com/windmill-labs/windmill/commit/665f83e1f438e34d006429889d51a5fb6a6b6176))
* migrate slack resource-connect oauth to v2 ([#10836](https://github.com/windmill-labs/windmill/issues/10836)) ([9fa8159](https://github.com/windmill-labs/windmill/commit/9fa8159ad16204cab52fd18a34a48ebf13f800f6))
* recover from unresolvable AI session links instead of a dead end ([#10854](https://github.com/windmill-labs/windmill/issues/10854)) ([e38c449](https://github.com/windmill-labs/windmill/commit/e38c449007f27b952808cba5aa812441f2ce5946))
* require admin on workspace tarball settings export ([#10817](https://github.com/windmill-labs/windmill/issues/10817)) ([46c363f](https://github.com/windmill-labs/windmill/commit/46c363ffa4bc72bef6b367ece4bdbeef5e0eadc9))
* restrict filesystem workspace storage to debug builds ([#10864](https://github.com/windmill-labs/windmill/issues/10864)) ([8b80b09](https://github.com/windmill-labs/windmill/commit/8b80b09f33d311f0881678577ca6004c12d97c22))
### Performance Improvements
* index the suspended-job resume test instead of filtering it ([#10863](https://github.com/windmill-labs/windmill/issues/10863)) ([69320b2](https://github.com/windmill-labs/windmill/commit/69320b28f615b897a92f580bd5961c41e5c29951))
Any other place a property can be picked into: the loop iterator, skip and early-stop predicates, the retry condition, a branch predicate, timeout. Its prop picker opens in a popover from the connect button rather than taking a pane.
_Avoid_: JS field, code input
### Permissions
**Member**:
A user or group granted a role on a folder, a group, or an item's extra ACL. The list of them is
"Members (n)" everywhere it is shown, and one is added with "Add member".
_Avoid_: participant, collaborator, owner, ACL entry, permission (that names the concept, not the people)
**Role**:
The access level a member holds: viewer, writer or admin on a folder; member or admin on a group.
Viewers read, writers also edit, admins also manage the members. A group role of **manager** —
manages the group without belonging to it — is a legacy state the UI shows and can leave, but
offers no way to enter.
_Avoid_: permission level, access level, rank
**Owner**:
Reserved for the path prefix that says where an item lives — `u/alice` or `f/team`. A folder's
`owners` column in the database is its admin members; call those admins, never owners, in the UI.
# A one-step flow is as well checked by running the step as the whole flow, so both
# count: what matters is that the run is against the draft.
requiredToolsAnyOf:
- [test_run_flow, test_run_step]
# The draft is what the user asked to check, and run_flow would run the deployed
# version instead — the edit would not be in what ran.
forbiddenToolsUsed:
- run_flow
- call_api_endpoint
- deploy_workspace_item
# The judge cannot observe runs, and the edit's content is already pinned by
# global-test5 on this fixture; what this case guards is where the run went.
skipJudge:true
judgeChecklist:
- creates an AI draft of f/evals/global/process_invoice applying 8% tax
- does not deploy or save the draft
- id:global-undo-created-draft
prompt:|-
Create a draft Postgres resource at `u/admin/scratch_db` for host db.example.com port 5432, database `orders`, user `app`, and tell me what fields it ended up with.
@@ -2365,3 +2503,37 @@
- the step uses the workspace's anthropic resource f/evals/global/anthropic_main
- the model is the Opus one the user asked for, taken from the models that resource serves
- the diff flow input reaches the agent
# The failure this pins: passing a resource as `{"$res": "<path>"}` (or as a bare path), which
# reaches the script unresolved because the backend only substitutes a string value that itself
# starts with `$res:`. The mock preview echoes args back and reports success, so nothing in the
# loop corrects a wrong shape — the arg form is the whole test.
- id:global-run-arg-resource-reference
prompt:|-
Run `f/evals/global/github_repo_stats` against the `windmill-labs/windmill` repo, passing our
GitHub credentials at `f/evals/global/github_main` as its `gh_auth` input, and tell me whether
"query":"SELECT\n COUNT(*)::bigint AS \"total!\",\n COUNT(*) FILTER (WHERE name = ANY($2::text[]))::bigint AS \"replacing!\"\n FROM ai_skill\n WHERE workspace_id = $1",
"query":"INSERT INTO metrics (id, value)\n VALUES ($1, to_jsonb((\n SELECT EXTRACT(EPOCH FROM now() - scheduled_for)\n FROM v2_job_queue\n WHERE tag = $2 AND running = false AND scheduled_for <= now() - ('3 seconds')::interval\n ORDER BY priority DESC NULLS LAST, scheduled_for LIMIT 1\n )))",
"query":"SELECT COALESCE(u.username, p.username, CASE WHEN p.email IS NOT NULL THEN d.email END) as \"username?\",\n d.created_at as \"draft_saved_at!\"\n FROM draft d\n LEFT JOIN usr u\n ON u.workspace_id = d.workspace_id\n AND u.email = d.email\n LEFT JOIN password p\n ON p.email = d.email\n AND p.super_admin = true\n WHERE d.workspace_id = $1\n AND d.path = $2\n AND d.typ = $3\n AND (d.email IS NULL OR d.email <> $4)\n AND (d.email IS NULL OR u.username IS NOT NULL OR p.email IS NOT NULL)\n ORDER BY d.email NULLS LAST",
"query":"SELECT\n COALESCE(dt.value->'database'->>'resource_type', 'unknown') AS \"kind!\",\n COUNT(*)::BIGINT AS \"count!\"\n FROM workspace_settings ws,\n LATERAL jsonb_each(ws.datatable->'datatables') dt\n WHERE jsonb_typeof(ws.datatable->'datatables') = 'object'\n GROUP BY 1",
"query":"\n SELECT\n (SELECT MIN(day) FROM guest_activity) AS since,\n (SELECT COUNT(DISTINCT email) FROM guest_activity\n WHERE day > CURRENT_DATE - 30)::INT AS \"guest_count!\",\n (SELECT COUNT(DISTINCT email) FROM guest_activity\n WHERE jwt_entry AND day > CURRENT_DATE - 30)::INT AS \"guest_jwt_count!\",\n (SELECT COUNT(DISTINCT workspace_id) FROM guest_activity\n WHERE day > CURRENT_DATE - 30)::INT AS \"guest_workspace_count!\",\n (SELECT COUNT(*) FROM workspace_settings ws JOIN workspace w ON w.id = ws.workspace_id\n WHERE ws.guest_access_enabled AND NOT w.deleted)::INT AS \"guest_enabled_workspace_count!\",\n (SELECT COUNT(*) FROM workspace WHERE NOT deleted)::INT AS \"workspace_count!\"\n ",
"query":"INSERT INTO guest_activity (email, workspace_id, day, jwt_entry)\n VALUES ($1, $2, CURRENT_DATE, true)\n ON CONFLICT (email, workspace_id, day)\n DO UPDATE SET jwt_entry = true, last_seen_at = now()\n WHERE NOT guest_activity.jwt_entry\n RETURNING 1 AS \"audited!\"",
"query":"DELETE FROM draft dest\n WHERE dest.email = $1\n AND EXISTS (SELECT 1 FROM draft src\n WHERE src.email = $2\n AND src.workspace_id = dest.workspace_id\n AND src.path = dest.path\n AND src.typ = dest.typ)",
"query":"DELETE FROM otel_traces WHERE ctid IN (\n SELECT ctid FROM otel_traces\n WHERE start_time_unix_nano < EXTRACT(\n EPOCH FROM now() - ($1::bigint::text || ' s')::interval\n )::bigint * 1000000000\n LIMIT $2\n )",
"query":"\n SELECT (\n SELECT elem\n FROM jsonb_array_elements(git_credentials) AS elem\n WHERE elem->>'repo_identity' = $2\n )\n FROM workspace_settings WHERE workspace_id = $1 FOR UPDATE\n ",
"query":"WITH slots AS (\n SELECT id, slot, min(t) AS first, max(t) AS last, max(v) AS peak,\n (min(ARRAY[t, v]))[2] AS first_value, (max(ARRAY[t, v]))[2] AS last_value,\n (max(ARRAY[t, climbing]))[2] = 1 AS last_climbing,\n COALESCE(bool_and(climbing = 1) AND max(since) - min(since) < $4, false) AS ramp,\n max(ARRAY[t, since]) FILTER (WHERE climbing = 1) AS last_climb\n FROM (\n SELECT id, t,\n CASE jsonb_typeof(value)\n WHEN 'number' THEN value::double precision\n WHEN 'object' THEN t - (value->>'since')::double precision\n END AS v,\n (value->>'since')::double precision AS since,\n (jsonb_typeof(value) = 'object')::int::double precision AS climbing,\n greatest(floor((t - $1::double precision) / $2::double precision), -1)::int\n AS slot\n FROM (\n SELECT id, value, EXTRACT(EPOCH FROM created_at)::double precision AS t\n FROM metrics\n WHERE id LIKE 'queue_%'\n AND created_at > to_timestamp($1::double precision - $3::double precision)\n ) m\n ) s\n WHERE v IS NOT NULL\n GROUP BY id, slot\n )\n SELECT id AS \"id!\", slot AS \"slot!\", first AS \"first!\", last AS \"last!\",\n greatest(peak, CASE WHEN last_climb[1] < last THEN (\n SELECT EXTRACT(EPOCH FROM min(n.created_at))::double precision\n FROM metrics n\n WHERE n.id = slots.id AND n.id LIKE 'queue_%'\n AND n.created_at > to_timestamp(last_climb[1] + 0.001)\n AND n.created_at <= to_timestamp(last + 0.001)\n ) - last_climb[2] END) AS \"peak!\",\n first_value AS \"first_value!\", last_value AS \"last_value!\",\n last_climbing AS \"last_climbing!\", ramp AS \"ramp!\"\n FROM slots\n ORDER BY id, slot",
"query":"UPDATE token SET label = $1\n WHERE email = $2 AND token_prefix = $3\n AND (label IS NULL OR (\n label <> 'session'\n AND lower(label) NOT LIKE 'ephemeral%'\n AND label <> 'debugger-token'\n AND label NOT LIKE 'mcp-oauth-%'\n ))\n RETURNING token_prefix",
"query":"UPDATE token SET label = $1\n WHERE email = $2 AND token_prefix = $3\n AND (label IS NULL OR (\n label <> 'session'\n AND label <> 'guest_session'\n AND lower(label) NOT LIKE 'ephemeral%'\n AND label <> 'debugger-token'\n AND label NOT LIKE 'mcp-oauth-%'\n ))\n RETURNING token_prefix",
"query":"UPDATE draft SET value = to_json(jsonb_set(to_jsonb(value), ARRAY['policy', 'on_behalf_of'], to_jsonb($1::text))) WHERE typ IN ('app', 'raw_app') AND value->'policy'->>'on_behalf_of' = $2",
"query":"SELECT path FROM asset WHERE workspace_id = 'test-workspace' AND kind = 'dbt' AND usage_path = 'u/test-user/ingest' AND usage_access_type = 'w'",
"query":"UPDATE draft SET value = to_json(jsonb_set(to_jsonb(value), ARRAY['policy', 'on_behalf_of_email'], to_jsonb($1::text))) WHERE typ IN ('app', 'raw_app') AND value->'policy'->>'on_behalf_of_email' = $2 AND (value->'policy'->>'on_behalf_of' IS NULL OR value->'policy'->>'on_behalf_of' NOT LIKE 'g/%')",
"query":"WITH queue_metrics as (\n SELECT id, value, created_at\n FROM metrics\n WHERE id LIKE 'queue_%'\n AND created_at > now() - interval '14 day'\n )\n SELECT id, array_agg(json_build_object('value', value, 'created_at', created_at) ORDER BY created_at ASC) as \"values!\"\n FROM queue_metrics\n GROUP BY id\n ORDER BY id ASC",
"query":"\n SELECT elem->'credential'\n FROM workspace_settings, jsonb_array_elements(git_sync->'repositories') AS elem\n WHERE workspace_id = $1 AND elem->>'git_repo_resource_path' IN ($2, $3)\n ",
"query":"WITH producer AS (\n SELECT 'dbt://' || a.path AS trigger_ref, a.usage_path, s.language\n FROM asset a\n JOIN script s ON s.workspace_id = a.workspace_id AND s.path = a.usage_path\n AND s.archived = false AND s.deleted = false\n WHERE a.workspace_id = $1 AND a.kind = 'dbt'\n AND a.usage_kind = 'script' AND a.usage_access_type IN ('w', 'rw')\n AND 'dbt://' || a.path = ANY($2)\n )\n SELECT DISTINCT st.trigger_ref || ' → ' || st.runnable_path AS \"edge!\"\n FROM script_trigger st\n WHERE st.workspace_id = $1 AND st.trigger_kind = 'asset'\n AND st.trigger_ref = ANY($2)\n AND EXISTS (SELECT 1 FROM producer p\n WHERE p.trigger_ref = st.trigger_ref\n AND p.usage_path <> st.runnable_path\n AND p.language = 'dbt')\n AND NOT EXISTS (SELECT 1 FROM producer p\n WHERE p.trigger_ref = st.trigger_ref\n AND p.usage_path <> st.runnable_path\n AND p.language <> 'dbt')\n ORDER BY 1",
"query":"INSERT INTO dbt_environment_state (workspace_id, script_path, environment, job_id,\n manifest)\n SELECT $1::varchar, $2::varchar, 'main||analytics|wh'::text, $3::uuid, '{}'::text\n WHERE EXISTS (SELECT 1 FROM script\n WHERE workspace_id = $1 AND path = $2\n AND deleted = false AND archived = false\n AND language = 'dbt'\n AND (hash = $4 OR $4 = ANY(parent_hashes)))\n ON CONFLICT (workspace_id, script_path, environment) DO UPDATE SET job_id = EXCLUDED.job_id",
"query":"SELECT p.asset_path AS \"from_path!\", e.parent_column AS \"from_column!\",\n c.asset_path AS \"to_path!\", e.child_column AS \"to_column!\",\n e.lineage_kind AS \"kind!\"\n FROM unnest($2::text[], $3::bigint[], $4::uuid[])\n AS o(script_path, script_hash, job_id)\n JOIN dbt_column_edge e ON e.workspace_id = $1\n AND e.script_path = o.script_path\n AND e.job_id = o.job_id\n -- `=` still, with the NULL-to-NULL case\n -- spelled out and gated on the pin: a\n -- version-less row's hash is NULL on both\n -- sides, which `=` never matches, but\n -- `IS NOT DISTINCT FROM` would cost the\n -- equality its index bound everywhere else.\n AND (e.script_hash = o.script_hash\n OR ($5::text IS NOT NULL\n AND o.script_hash IS NULL\n AND e.script_hash IS NULL))\n JOIN dbt_node p ON p.workspace_id = e.workspace_id\n AND p.script_path = e.script_path\n AND p.script_hash IS NOT DISTINCT FROM e.script_hash\n AND p.job_id = e.job_id\n AND p.unique_id = e.parent_unique_id\n JOIN dbt_node c ON c.workspace_id = e.workspace_id\n AND c.script_path = e.script_path\n AND c.script_hash IS NOT DISTINCT FROM e.script_hash\n AND c.job_id = e.job_id\n AND c.unique_id = e.child_unique_id\n WHERE e.lineage_kind IN ('copy', 'mod')\n AND p.asset_path IS NOT NULL AND c.asset_path IS NOT NULL",
"query":"SELECT 1 FROM script\n WHERE workspace_id = $1 AND path = $2\n AND deleted = false AND archived = false AND language = 'dbt'\n AND (hash = $3 OR $3 = ANY(parent_hashes))\n FOR SHARE",
"query":"SELECT COALESCE(c.id, r.id) AS \"id!\", r.value AS \"value?\",\n EXTRACT(EPOCH FROM r.created_at)::double precision AS \"at?\",\n EXTRACT(EPOCH FROM now() - r.created_at)::double precision AS \"age?\"\n FROM unnest($1::text[]) AS c(id)\n FULL JOIN (\n SELECT DISTINCT ON (id) id, value, created_at\n FROM metrics\n WHERE id LIKE 'queue_%' AND created_at > now() - make_interval(secs => $2)\n ORDER BY id, created_at DESC\n ) r ON r.id = c.id",
"query":"UPDATE log_file SET indexed_at = now()\n FROM unnest($1::text[], $2::text[]) AS c(hostname, file_path)\n WHERE log_file.indexed_at IS NULL\n AND log_file.hostname = c.hostname\n AND log_file.file_path = c.file_path",
"query":"\n UPDATE workspace_settings\n SET git_credentials = (\n SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb)\n FROM jsonb_array_elements(git_credentials) AS elem\n WHERE elem->>'repo_identity' IS DISTINCT FROM $2\n ) || jsonb_build_array($3::jsonb)\n WHERE workspace_id = $1\n ",
"query":"SELECT\n hostname,\n mode::text,\n worker_group,\n log_ts,\n file_path,\n ok_lines,\n err_lines,\n json_fmt\n FROM log_file\n WHERE log_ts > $1\n ORDER BY log_ts ASC LIMIT $2",
"query":"SELECT\n hostname,\n mode::text,\n worker_group,\n log_ts,\n file_path,\n ok_lines,\n err_lines,\n json_fmt\n FROM log_file\n WHERE indexed_at IS NULL\n ORDER BY log_ts ASC, hostname ASC LIMIT $1",
"query":"WITH running AS (\n SELECT tag, count(*) AS n FROM v2_job_queue WHERE running = true GROUP BY tag\n )\n SELECT t.tag AS \"tag!\", COALESCE(r.n, 0) AS \"running!\",\n (SELECT count(*) FROM worker_ping w\n WHERE w.ping_at > now() - interval '1 minute' AND w.custom_tags @> ARRAY[t.tag]\n ) AS \"workers!\"\n FROM (SELECT tag::text FROM running UNION SELECT unnest($1::text[])) t(tag)\n LEFT JOIN running r ON r.tag = t.tag\n ORDER BY t.tag",
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.