mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-03 16:02:12 +00:00
ca44043e12d4713d67ca13eb546eef9d00cc965e
2281
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ca44043e12 |
feat: let operators compose flows when the workspace grants the right (#11228)
* feat: let a workspace withdraw operator schedule and trigger writes Operators can create, edit and delete schedules and triggers today through the API, CLI and MCP, while the operator_settings flags beside them only hide those pages. An admin who wants operators to see what is scheduled without letting them change it cannot express that. Add manage_schedules and manage_triggers as enforced settings, gated at the schedule handlers and at the generic TriggerCrud routes so every trigger kind is covered by one check. They name capabilities operators already hold, so they are granted unless withdrawn, and absence has to mean "never configured" rather than a value. The read coalesces to true; the update endpoint merges into the stored jsonb with the two fields as Option<bool>, so an omitted key keeps what is stored. operator_settings is git-synced as a whole object, so a settings file written before these keys existed reaches the endpoint on every pull, and a serde or SQL default of either polarity would turn that pull into a silent withdrawal or restoration. The rights are read through a per-process cache, so withdrawing one publishes a notify_event that drops the entry on every replica. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dsf6VC4MVLisiEoeQkgbr4 * feat: let operators compose flows when the workspace grants the right Adds operator_settings.builder_flows: a workspace setting that lets every operator compose flows out of runnables that already exist. It does not make them authors. The boundary the operator role draws is authoring code and running arbitrary code, and this does not move it: check_flow_is_composition_only walks the value and refuses anything carrying code, including the shapes an obvious walk misses (code hoisted into a flow_node, an AI agent step's tools, and a linked ai_agent resource whose tool list is resolved at run time). What the walk cannot settle it returns for the caller to authorize under RLS: the worker tags the steps pin, every runnable they reference, and the (path, hash) of every version-pinned step. Composing a path is enough to run it and to run it as whoever it runs as, since the worker resolves a step's path with the root DB handle and adopts that runnable's on_behalf_of. A pinned hash needs its own check because dispatch ignores the path beside it. The gate runs on every write and on both request-supplied-value paths, flow preview and flow dependencies, or either becomes the way to run what the write path refuses. Operators of a builder workspace consume a full author seat; the EE companion carries the counting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dsf6VC4MVLisiEoeQkgbr4 * feat: enforce operator write rights on the router and in the UI Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: close the capture gap and gate the trigger editors' write actions Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: gate acl writes and the native trigger drawer behind manage rights Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: refuse operator writes with 403 and gate sharing at the drawer Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * perf: resolve identity in the operator write gate only for writes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: gate the suspended-jobs actions and stop the route check refusing reads Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: explain the empty-state create button when operator writes are withdrawn Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: audit operator settings changes and fold path writes into native rows Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: open locked editors read-only and group the operator settings Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: skip email and azure lookups on editor open while triggers are locked Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs: state each operator-rights rationale once in comments Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: address CI review findings on operator write rights Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: keep capture move gated and skip it in the builders while locked Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: refuse builder-rights violations with 403 so operators stay logged in Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor: trim duplication in the operator builder gates Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: hide build app from builder operators on the flow page Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore: point at the companion EE PR merged with EE main Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: keep a builder's drafts list loading past drafts they cannot write Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: hide saved agents from builder operators in the step picker Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore: note the inlined seat rule and drop orphaned sqlx entries Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: stop a builder's step test from logging them out Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: refuse a builder's dependency job on a path it cannot write Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: keep builders from adding dynamic dropdown code to a flow A flow's dropdown code runs as whoever loads its form, so a builder may keep or drop the code stored on the flow it updates, never add or change it. The builder's editor hides the dropdown types and code, and previews options through the deployed flow; the inline dropdown refusal is a 403 so it no longer logs operators out. Also trims rationale comments repeated across sites. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: show why saving operator settings failed The seat-cap refusal on granting builder rights explains what to do; the toast now carries the server's message instead of a generic failure. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: check builder flow drafts like deploys and treat dropdown code as code A developer who loads a builder's flow draft in the editor runs its dynamic dropdown code as themselves, so a builder's draft now passes the same checks as a deploy. Dropdown code is refused like step code rather than kept or dropped, which also removes the exact-match comparison that refused builders over whitespace. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: bill a builder workspace's operators as developers on cloud The cloud seat count behind the Premium page, the sidebar usage and the fork cap still weighed every operator at half a seat, while the builder right makes them authors. The out-of-repo invoicing job must follow the same rule. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: check a builder's flow draft as it will be stored Draft storage strips NUL escapes after the builder check, so a key ending in one (value\u0000, x-windmill-dyn-select-code\u0000) passed the check as an unknown field and was stored under its plain name. The check now reads the sanitized text. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: list a builder's flow drafts and hide hub imports from builders A builder's undeployed flows now appear in the home list, the flow list and the folder counts. Hub project imports and templates bring scripts and apps along, so builders are no longer offered them. The docs record builders' JavaScript expressions as an accepted risk. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: keep the stored builder right when a settings payload omits it A git-synced settings file written before the key existed withdrew the right on every push. builder_flows now follows the manage_* rights: an omitted key leaves the stored value, and the CLI does not count it as a difference. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: word builder refusals by what the flow contains, test the tag refusal A builder refused on a developer's flow never changed its code, so the refusals now describe the flow ("has inline code, so only a developer can edit this flow") rather than an authoring attempt. The grant confirmation uses the neutral dialog: granting changes billing but destroys nothing. The integration test pins the refusal of a worker tag the workspace cannot use. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: read builder rights from the operating workspace, gate the flow page's audit logs entry Builder rights now come from useOperatorBuilderFlows(), next to the schedule and trigger locks, so an editor embedded for another workspace answers about that workspace; the legacy AI chat, one instance for the whole app, reads the navigation workspace. The flow page's Audit logs entry follows the operator audit_logs setting now that builders open that menu. Operator settings reset every value on load, null settings included, so nothing carries over from the previous workspace. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: pick a dynamic dropdown's code source by the operating user's role The flow input editor, the flow test panel and the flow chat send the dropdown request to the operating workspace, so they now also choose inline versus deployed code by the role held there, through useOperatingUser(), instead of the navigation workspace's. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to 40ac1c5f8cbce3843b582d9b392d3f3cc7eca3e6 This commit updates the EE repository reference after PR #815 was merged in windmill-ee-private. Previous ee-repo-ref: 31c9e66884b8ca805b20bbfad41fc428fbedbc0e New ee-repo-ref: 40ac1c5f8cbce3843b582d9b392d3f3cc7eca3e6 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> |
||
|
|
077709b914 |
feat: rename agent memory options to On and Legacy, explain each (#11442)
* feat: rename agent memory options to On and Legacy, explain each Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: default the chat agent example to On memory and align the schema copy Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: offer Legacy memory only to steps that hold it, explain under the toggle Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: keep the off memory hint true on the saved-agent editor Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: state the 128k fallback in the context window description Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1d8d4a16b4 |
fix(cli): resolve fork specific items from the parent workspace entry (#11436)
* test(cli): stop CLI tests resolving a just-published windmill-client Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test: quote the Windows cert path in the pg attach assertion Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(cli): note when the windmill-client pin must move Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(cli): resolve fork specific items from the parent workspace entry Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(cli): keep fork resolution to the id-guarded inference Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
cec8eaeadb |
test: fix CLI and Windows cargo test failures on 1.820.0 (#11429)
* test(cli): stop CLI tests resolving a just-published windmill-client Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test: quote the Windows cert path in the pg attach assertion Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(cli): note when the windmill-client pin must move Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
f890355211 |
chore(main): release 1.820.0 (#11404)
* chore(main): release 1.820.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> |
||
|
|
c1b59f70dd |
feat(ai-agent): add compaction memory that summarizes older context (#10928)
* feat(ai-agent): add autocompacted memory that summarizes older context Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): compact on final-answer turns and count what a turn appended Address the pre-push review findings on the compaction path: - A turn the model answers without a tool call left the agent loop on its first iteration, so a chat-shaped step never compacted and reloaded the whole conversation on every later turn. Compaction now also runs after the loop. - The trigger measured only the last request, so a single large tool result could carry the next one past the window without ever crossing 80%. - The summarization call re-sent the usage-tracking request shape on endpoints the loop had already learned to drop it for. - The flat 8000-token summary reserve swallowed the whole target on a small context window, leaving one message in the tail and summarizing the rest. - A response cut off inside the <analysis> scratchpad was accepted as a summary. - The chat-mode memory default was a shared object the step form edited in place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): keep Anthropic prompt counts and compact once per response Address the first CI review round on the compaction path: - Anthropic's streaming parser dropped `message_start`, the only event carrying the prompt-side counts, so a native Anthropic run reported no input tokens at all and compaction fell back to a character estimate. - A loop that exits without issuing another request — a structured-output turn does — reached the post-loop pass still holding the previous measurement and compacted a second time, or retried a failure with nothing changed. - The summarization call inherited the step's `max_completion_tokens`; a low one truncates the summary inside its scratchpad, which counts as a failure and disables compaction after three of them. - A fired trigger that found nothing to summarize said nothing. - Memory already over the window — a lowered `context_window`, or a step moved over from `auto` — had no way back, since compaction only ran after an accepted request. It now also runs once before the first one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): state the summary's own completion cap and drop the pre-flight pass - The summarization call asked for no completion cap at all, which is "uncapped" only on the OpenAI-shaped providers: Anthropic substitutes 64000, over several Claude models' output ceiling, and Bedrock leaves the model's own small default, short enough to cut the response off inside its scratchpad. It now asks for the reserve the split already set aside, raised to the step's cap when that is larger. - Compaction no longer runs before the first request. The fallbacks the loop learns from a rejection are not known that early, so on exactly the endpoints that need them the summarization was malformed by construction: it failed, spent a strike, and the first agent request still carried the oversized conversation. A memory already past the window is repaired on the turn after a request the endpoint accepts, rather than by a pass that cannot succeed there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): ask the summary for exactly the room the split reserved The split scales its reserve down on a small window while the request asked for a flat 8000, so the two diverged below an 80k window: on a 4k/8k model the cap alone exceeded the window and every summarization was refused, and on a 20k one a full-length summary could land the conversation back over the trigger and compact its own previous summary on the next response. Both now read one `summary_reserve_tokens`. The call also no longer inherits the step's reasoning effort. Every provider counts thinking against that same budget, so a high-effort model could spend the whole reserve before writing anything and return a summary cut off inside its scratchpad; the compaction prompt asks for an `<analysis>` block, which is the reasoning this call needs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): charge the compaction budget for tools and the system prompt The tail budget was the whole target, but a request also carries the system prompt compaction keeps and the tool definitions, which are not in the message list at all. On a small window those are most of it: a tail sized to the full target left the next request back over the trigger, compacting again every response, and the no-usage estimate missed the tool schemas entirely so it could fail to trigger at all. Both now account for them. The reserve also gains a floor. It is the summary's output cap as well as the room the split leaves, and scaled down without one a small window gave a structured nine-section summary a few hundred tokens — truncated inside its scratchpad every time, which is discarded, which switches the mode off after three. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): count Gemini's tool-use prompt tokens in an agent step's usage Gemini splits a tool-using turn's input across `promptTokenCount` and a disjoint `toolUsePromptTokenCount`, and its thinking apart from `candidatesTokenCount`. The agent step's parser read only the headline fields, so every tool-using turn under-reported both — and the compaction trigger, which runs off the reported prompt, could not see the tool results that grew it. It now goes through the same helpers the proxy path already used. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): calibrate the compaction estimate against the measured prompt Two rounds running, the finding was "the character estimate cannot see input X" — tool schemas, then S3 attachments, which are short paths in the message list and whole images by the time a provider counts them. Enumerating those is a list that only grows, so the estimate is now scaled to the one number that is ground truth: what the provider charged for the last request. Attachments, tokenizer drift and whatever comes next fall out of that, because the estimate is only ever used relative to itself. Also stop the Gemini helpers turning an absent count into `Some(0)`. Downstream, absent means "fall back to estimating the conversation" while zero reads as an empty prompt and would hold the trigger below its threshold for the whole run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): charge attachments what they cost and let a heavy short prefix compact The calibration conserved the conversation's total cost but spread it by character count, so an attachment — a short S3 path in the message list, a whole image or PDF once a provider expands it — was charged to the text messages around it and stayed nearly free in the split. It now carries a nominal cost of its own, which the calibration corrects a residual on rather than the whole gap. The four-message minimum also refused exactly the case that fix is for: an attachment arriving on the first or second turn can pass the trigger before four removable messages exist, and summarizing even one of them saves most of the prompt. A prefix worth a quarter of the window is now enough on its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): never summarize a prefix holding only a previous summary The message-count floor was carrying a second job: a fresh summary sits in a one or two message prefix, so requiring four declined it. The share threshold added last commit admits it, and a summary is reserve-sized by construction — so the post-compaction shape could spend one summarization per response swapping a summary for another the same size, shrinking nothing and losing fidelity each time. A previous summary no longer counts towards that threshold. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): take the context window from the model and drop the estimate calibration Brings compaction in line with how the AI session does the same job, which had already answered these three questions. - The window is looked up from the model. `MODEL_CONTEXT_WINDOWS` in `windmill-ai/src/model_context.rs` mirrors the session's table in `copilot/modelConfig.ts`, entry for entry and with the same matching rules; each side points at the other, since a model added to one and not the other compacts at two different sizes. A step's `context_window` becomes the override for what the lookup cannot serve, and chat mode writes none. - Provider usage is normalized where the provider's quirk is, not at the consumer. `TokenUsage::with_cache_beside_input` raises `input_tokens` to the whole prompt for Anthropic and Bedrock, which report their cached prefix beside it; the OpenAI shape already counts it inside. `prompt_tokens()` is then just `input_tokens`, rather than inferring the shape from whether a write count is present. - The estimator is no longer calibrated against the measured prompt. The session uses the provider's count when it has one and a chars/4 estimate otherwise, with nothing in between, and a tail sized a little wrong only compacts again a turn later. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * feat(ai-agent): summarize memory down to what the database can store Without an instance object store, memory is a 100KB database row cut from its oldest message, the summary included, so compaction on a mainstream model never got to keep anything across runs. A step that persists there now runs its post-loop compaction pass against the smaller of the model's window and the cap at chars/4, about 25k tokens: the loop keeps the whole window, and what is written is a summary plus a tail that fits. The run logs when that pass summarizes, and how many messages the write dropped when one still overshoots. The editor's storage warning on the option is removed: nothing exposes the instance storage to it, so it keyed on the workspace S3 setting, which is unrelated to where memory goes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): get a complete, billed summary out of every provider Compaction against the real providers turned up four things the stub could not: Gemini and OpenAI's reasoning models think by default and bill it against the same cap the summary must fit in, so the summarization request now asks them for their least (none, low); an OpenAI Responses call that hits max_output_tokens ends in response.incomplete, whose usage the parser dropped, so that summarization went unbilled; a summary that quotes </summary> when it describes its own instruction was cut off at the quote, on the agent step and the AI session alike; and the prefix could end on an unanswered user message, after which the instruction reads as part of that turn (Anthropic merges the two outright). The tail now starts on a user message, and both prompts tell the model the instruction is not part of the conversation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): compact down to half the window, on the agent step and the AI session The gap between the 80% trigger and the target is what one compaction buys, and every summarization request carries most of the window. At a 70% target a 128k model summarized about 13k tokens of prefix for a summary of up to 8k, so each ~100k-token request bought a few turns of room before the next one re-summarized the previous summary. At 50% the same request frees about 30k. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): drop the workspace-S3 memory hint and state the database bound in the tooltip The memory field warned that memory is kept in the database whenever the workspace had no S3 storage. That setting has no bearing on where memory goes: the instance object store decides, and nothing exposes it to the editor. The field's tooltip now describes both memory kinds and states the database bound unconditionally; the run log says what happened. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): send the summarizer its tool history as text The summarization request carries no tool definitions, and Bedrock's Converse API rejects toolUse/toolResult blocks that arrive without them, so on Bedrock every summarization of a prefix holding a tool call failed silently until the breaker tripped. The prefix's tool calls and results now reach the summarizer rendered as text, on the agent step and in the AI session's compaction, which goes through the same proxy. Also drops the TokenUsage::prompt_tokens accessor, which had become a plain read of the normalized input_tokens, and shortens the context window field's description. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): price attachments from the provider count, bound storage in bytes, effort per pro model Addresses two Codex rounds and a leftovers audit. - Attachments were priced at a flat 1500 tokens in the split, so a multi-page PDF (tens of thousands of tokens to the provider, a short S3 path in the message list) could be kept in the tail or leave no prefix worth summarizing. They are now priced from the provider's count for the request that carried them, less that request's text, with the 1500 floor where nothing was counted. - The database storage bound measured the provider's token count, but the 100KB cap is bytes and repetitive text packs several characters per token. The persist pass now measures the serialized conversation. - The summarizer forced `low` on every reasoning model, which the pro variants reject (gpt-5-pro takes only high, gpt-5.2-pro starts at medium); they now get no effort. - Dropped the unused prompt_tokens accessor and its orphaned assert, an unused PartialEq, a needlessly public lookup, and fully-qualified Gemini calls; refreshed stale comments and the memory_id schema doc; regenerated the flow schema artifacts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): evict a heavy attachment into the summarized prefix, not the tail Pricing attachments from the provider count was not enough on its own: a leading attachment is a user message, and the boundary rule pulled the last unanswered user turn back into the kept tail to keep it with its answer. For a heavy attachment that dragged it into the tail — or, at the front, emptied the prefix — so it was never summarized and rode every request. The boundary now moves forward instead, keeping that user turn and its answer in the summarized prefix. Verified on the running instance: a 25k-token PDF on a 30k window is summarized out on the turn it overflows, and later turns drop from 26k to ~1.5k tokens. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): keep the forward boundary move off tool results and the prefix start The forward move that keeps an unanswered user turn out of the tail had two edges the third Codex round found: advancing past the user could land the boundary on a tool result (its tool_calls then summarized away, orphaning it), and with no system prompt the summarizable prefix starts at 0, so a trigger firing while the tail estimate fit everything indexed below the start and panicked the task. The forward scan now skips tool-opening boundaries, and the move is guarded above the prefix start. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): drop the step temperature from the summary request OpenAI's reasoning models (gpt-5-mini, gpt-5.1, gpt-5.2) reject `temperature` alongside any reasoning effort but their own default, so a step configured with a temperature made every summarization fail once the summarizer forced a low effort — history then grew unchecked. The internal summary call now omits the step's temperature: a structured extraction does not need a set one, and omitting it sidesteps each provider's temperature-versus-reasoning rules. Confirmed against the API that low + temperature is refused on those models. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): compact an oversized loaded memory before the first request Compaction was reactive, taken only after a request the endpoint accepted, so the fallbacks the loop learns from a rejection are known first. But a memory loaded from an earlier run can already exceed this run's window — the step was switched to a smaller model, or a run under a wider one persisted more than fits — and that first request then overflows and fails the run, with every retry reloading the same history and failing again. A pass is now taken up front, off the character estimate, before the first request. It uses the default request shape; an endpoint needing a fallback may reject this one summary, which is non-fatal, and mainstream providers need none. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): under the storage bound, trigger on the max of bytes and model tokens The storage-bound pass measured only the serialized row size, so an attachment — a few bytes as an S3 path but nearly the whole model context — read as tiny and the pass skipped a compaction the model needed. It now takes the larger of the byte measure and the model's token count, since repetitive text is few tokens but many bytes and an attachment is the reverse; either being over must fire a pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): drop oldest turns when a summary cannot fit the window, as the AI session does An oversized loaded memory (a step switched to a smaller model, or an object-store run that persisted more than a later model's window holds) left a prefix larger than the summarizer's own window, so the summary request overflowed and failed, the memory was untouched, and every retry failed the same way. The AI session handles this by falling back from summarization to dropping the oldest turns down to the target; compaction here now does the same. When a summary cannot run — it failed, the breaker is tripped, or nothing is worth folding — the oldest turns are dropped until the conversation fits and opens on a user message, keeping the newest turn. The next request then always fits. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): drop whole turns only, keep the storage pass to bytes, refresh the count after a rewrite Three edges the seventh Codex round found, all in the drop-oldest fallback and the storage-bound measure: - drop_oldest_to_fit dropped to any point that freed enough, which could strand a tool result whose tool_calls went with the messages before it. It now drops whole turns only, always landing the boundary on a user message and never splitting the newest turn; a lone turn too big for the window is left whole rather than broken. - The storage-bound pass measured the whole model prompt against the shrunk 25k window, so a large tool roster and the system prompt — neither written to the row — tripped it on a conversation the row easily held. It measures the serialized bytes alone now; the model's own window is enforced by the in-loop passes and the pre-first-request pass, so the persisted size is all this pass is for. - A compaction rewrites the message list, so the provider's count for the request that produced it no longer lines up. The count is now cleared after any pass that rewrites the conversation, so a later pass measures the estimate over the actual messages instead of a stale, larger prompt (which could decline a summary that already fit and then drop it). The step temperature, no longer sent to the summarizer on any path, is dropped from the request struct. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): measure only the persisted messages against the storage cap Persistence strips the system prompt before writing the memory row, but the storage pass was serializing every message including it, so a large system prompt with a tiny conversation reported far over the storage trigger, and the fallback dropped the one real turn, run after run. The storage measure now serializes only the non-system messages, matching what the row actually holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix(ai-agent): run the model-window pass before the storage-bytes pass post-loop A turn the model answered without a tool call broke before the in-loop compaction check, so on database-backed memory its only pass was the storage one, which measures bytes. An attachment fills the model context but is a few bytes in the row, so that turn never compacted and a follow-up could overflow the model. The post-loop now runs a model-window pass first, off the provider's count, then the storage-bytes pass when the row is smaller than the model — both limits enforced for a chat-shaped step, not just the one that happens to bind. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix: simplify agent compaction and preserve execution history * fix: remove unused compaction history setting * fix: preserve answers and recover rejected agent context * refactor: make agent compaction transactional * fix: skip agent summaries that cannot fit retained context * fix: explain skipped agent context compaction * fix: retain recent agent memory when storage compaction cannot fit * fix: start retained agent memory at a user turn * fix: reject unsafe agent memory truncation on storage fallback * docs: clarify agent context window override scope * fix: keep recent turns verbatim when compaction memory outgrows storage Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix: keep the compaction summary out of the agent's answers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * fix: shorten the agent context window help text Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViJyjUmidDYV2m6ifQdLeH * chore: update ee-repo-ref to 942d4013f36edac1fc9a9addbdb02198db1c7a05 This commit updates the EE repository reference after PR #812 was merged in windmill-ee-private. Previous ee-repo-ref: 8ca1682ce6106ba6ea96894fbe606dac64102eb6 New ee-repo-ref: 942d4013f36edac1fc9a9addbdb02198db1c7a05 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> |
||
|
|
054109d855 |
fix: create workspace forks in the background, with progress (#11406)
* fix: create a fork in the background so a proxy timeout cannot cut it create_fork copies the whole workspace inside the request, which can run past the route timeout of an ingress in front of Windmill (Envoy's 15s default), and the UI then reports a failure for a fork still being made. create_fork?background=true now returns once the request is validated and records the copy in workspace_fork_creation, which the new fork_creation_status endpoint reads. The wizard and AI-session forks use it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: drain background forks on shutdown and fork in the background from the CLI Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: settle fork polls by the fork's existence, adopt in-flight creations Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat: show which part of the copy a fork being created is in The background copy reports its phase (data tables, settings, resources, scripts, flows, apps, drafts, triggers) through a watch channel. The heartbeat task records it on the fork's creation row as soon as it changes, and fork_creation_status returns it. The fork wizard shows it on its button, and the CLI logs each step. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: join a retried fork creation server-side, settle status by the fork's existence A retry from the same user and parent joins the creation in flight instead of being refused, so clients no longer match the refusal's wording, and another requester can never adopt it. The status route reports a fork that exists under its parent as completed, whatever its run's record says. Clients give up on failing polls after a time window rather than a count, which a rolling deploy can exhaust. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: drop fork-creation joins and existence probes A second request for a fork being created is refused again; only the AI-session fork, whose request never varies, waits for its own earlier one. Clients recognise a server without background forks by its synchronous answer instead of probing for a workspace by id, which could name another parent's fork. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: poll a background fork by the id of its own attempt create_fork?background=true answers with a creation id, and the status route reads that attempt only, for the user who started it. A retry that reuses the fork id is a new attempt, so a poller never reads another attempt's outcome. The AI-session fork no longer adopts a creation in flight, which it could not tell apart from someone else's. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: record a fork's completion in its own commit, resume a session's fork after reload The attempt is marked complete in the transaction that creates the fork, so the status never infers completion from a workspace that may belong to another request. An AI session keeps the creation id on its pending fork and, after a reload mid-copy, waits for that attempt instead of requesting the fork again. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
ede4103b00 |
feat: share AI guidance with the CLI skills and lint flow groups (#11397)
* feat: lint AI agent tool names and flow groups in wmill lint * refactor: assemble chat and CLI AI guidance from one topic table * feat: share flow groups, reuse and pipeline guidance with the CLI skills * feat: share raw app, data table and secret guidance between chat and CLI * docs: document the shared AI guidance source for contributors * fix: keep wmill lint running on flows with malformed collections * fix: reject skill descriptions that are not plain YAML text * fix: tighten fence typos, script base scope and app prompt order * fix: skip tool name checks on agent steps linked to a saved agent * fix: catch any misspelled prompt fence and soften the tool name claim * fix: align cli eval harness with the files and steps wmill init adds * fix: drop cli eval checks that expect unrequested deploy commands * fix: list ansible as mainless and c# Main in script base guidance |
||
|
|
8741843d2e |
feat: external instance cluster for data tables and Ducklake catalogs (#11197)
* fix pg_dump stuck on version 17 on nix * fix(datatables): refuse a malformed role annotation instead of ignoring it `-- Role operator`, `-- role operator;` and `-- role operator -- why` all failed the annotation parser's exact-match rule, so the query fell through to the data table's default role and ran, silently, under a login the author did not choose. Naming a role exists precisely to not do that. A leading comment whose first word is `role` is now an annotation attempt: the keyword matches case-insensitively, one trailing `;` is tolerated, and anything else is an error naming the line. Only callers that already know the target is a `datatable://` reference ever run this, so ordinary SQL keeps its comments. Also bumps the dev shell's postgres client to 18 — it trailed the server the dev database runs, which takes out every data table export, clone and fork-with-data. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR * fix(datatables): refuse a malformed role query string instead of ignoring it `?Role=analytics`, `?role=` and `?x=1&role=…` all fell through the reference parser's exact-match rule, so the connection resolved to the data table's default role and ran under a login the caller never asked for — the URI half of the same trap as a malformed `-- role` annotation. The key now matches case-insensitively, and anything else in the query string is an error naming it; `role` is the only parameter a reference takes. Callers that only need the entry keep a lenient `datatable_ref_name`, since they never act on the role. The DuckDB `ATTACH` parser propagates it rather than attaching under the default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR * fix(datatables): carry the role annotation into the row_to_json retry The retry rebuilds its SQL from `pruneComments(code)`, so the leading comment block never reached the second attempt — and with it the `-- role <name>` line that decides which login the query runs as. The retry connected as the data table's default role instead, so a query the first attempt was denied could succeed on the second, reported as "recovered with the row_to_json fix". Carry the leading comment block over. The retry itself is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BjfMkJyKzodxkobqGZ6Lqb * chore(datatables): don't mount the roles UI until the ACL editor lands Enforcement ships first. The permissions drawer is what turns roles on, and the catalog section is what creates them — both are only useful once there is a way to grant a role the privileges it needs, which arrives with the ACL editor. Left mounted they would offer a feature whose other half does not exist. The two components are complete and reviewed; only their call sites here are commented out, with a note pointing the follow-up PRs at them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BjfMkJyKzodxkobqGZ6Lqb * fix(datatables): honour `-- role: x`, and fix the DuckDB attach test Two review findings, both real. `attach_datatable_parses_name_and_role` never compiled: `parse_attach_datatable` returns `Result<Option<_>>` now and one call site kept a single `unwrap`. Its `?Role=analytics` case also asserted a refusal, contradicting the parser in the same commit, which matches the key case-insensitively. Replaced with the cases that are genuinely malformed, and a positive one for the cased key. `-- role: analytics` fell through to the default role — the silent fallback the strict parser exists to remove, for the spelling most likely to be typed. The keyword now accepts an optional colon, attached or spaced, while a word that merely starts with it (`rolebased`) is still not an attempt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BjfMkJyKzodxkobqGZ6Lqb * fix(datatables): clone a fork's pointer instead of failing after the copy Forking a fork with cloning left an orphan database. The preflight resolves the pointer and sees the governing entry, so both endpoints ran and filled the new database; `apply_forked_datatable` then refused the inherited pointer and rolled the fork back, stranding a registered `wm_fork_*` that no entry names and whose name blocks the retry. Refusing earlier would have been the smaller change, but forking a fork and cloning worked before pointers existed, so it would trade an orphan for a regression. Resolve what the pointer names and write the terminal entry the clone needs: the whole `database` object rather than a patch of its `resource_path`, since a pointer has none, and `reference` removed with it. Also accepts `-- role=x` and `-- Role = x`, two more spellings that fell through to the default role. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BjfMkJyKzodxkobqGZ6Lqb * fix(datatables): refuse to roll back the catalog while roles exist The down migration dropped the table and left every role behind: live Postgres logins whose passwords only that table carried, so after a revert Windmill could neither use, disable nor delete them, and re-applying could not recreate them because the names were taken. Cleaning up here is not possible either — dropping a role means reassigning what it owns in every instance database, and a migration runs in one — so it now refuses while the catalog is non-empty and says to delete the roles through instance settings, which does the cluster work. Also enforces the instance-only invariant the resolved-pointer clone relies on rather than only asserting it in a comment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BjfMkJyKzodxkobqGZ6Lqb * refactor(datatables): settle clonability in one place, before anything is created A clone is three stages a workspace apart — `create_pg_database`, then `import_pg_database`, then `apply_forked_datatable` inside the fork transaction. Only the third can roll back, and `CREATE DATABASE` is not transactional, so any refusal that lives there strands a registered `wm_fork_*` that no entry names and whose name blocks the retry. That orphan has now been fixed three times, most recently reintroduced by a guard added one commit ago. Patching each new refusal into the first endpoint is not the fix; having two places that can refuse is. `ensure_datatable_is_clonable` now answers every reason a copy can be refused and returns what it resolved, and the stage that writes the entry only does the work. Also takes an ACCESS EXCLUSIVE lock before the rollback guard counts, so a role created concurrently cannot slip between the check and the drop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BjfMkJyKzodxkobqGZ6Lqb * fix(datatables): let a retried clone reclaim its own leftover database A clone creates its target database one request before it copies into it, and the fork that would name it is written a request after that. Any failure in between — a pg_dump error, a bad restore, a dropped connection, the source's roles changing mid-flow — left a registered `wm_fork_*` that no entry names, and every retry then failed on its name. This predates data table roles. `create_pg_database` now reclaims such a leftover before creating: only a `wm_fork_*` database Windmill registered as a data table database and that no data table or ducklake entry names, in any workspace, archived ones included. The drop never terminates connections, so a clone still copying into it makes the reclaim fail instead of being cut off. It is limited to callers who administer the source — reaching it is not enough, since on a data table without roles every member reaches it — and anyone else gets the refusal an existing database always got. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Revert "fix(datatables): let a retried clone reclaim its own leftover database" This reverts commit |
||
|
|
651a6b01f5 |
chore(main): release 1.819.0 (#11364)
* chore(main): release 1.819.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> |
||
|
|
3974bbeac6 |
chore(main): release 1.818.0 (#11294)
* chore(main): release 1.818.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> |
||
|
|
30bb62cd25 |
fix(cli): stub the API client over its real exports in tests (#11363)
* fix(cli): stub the API client over its real exports in tests Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(cli): mock the API client once and dispatch to per-suite stubs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
9a1c6e5081 |
feat: let a workspace withdraw operator schedule and trigger writes (#11226)
* feat: let a workspace withdraw operator schedule and trigger writes Operators can create, edit and delete schedules and triggers today through the API, CLI and MCP, while the operator_settings flags beside them only hide those pages. An admin who wants operators to see what is scheduled without letting them change it cannot express that. Add manage_schedules and manage_triggers as enforced settings, gated at the schedule handlers and at the generic TriggerCrud routes so every trigger kind is covered by one check. They name capabilities operators already hold, so they are granted unless withdrawn, and absence has to mean "never configured" rather than a value. The read coalesces to true; the update endpoint merges into the stored jsonb with the two fields as Option<bool>, so an omitted key keeps what is stored. operator_settings is git-synced as a whole object, so a settings file written before these keys existed reaches the endpoint on every pull, and a serde or SQL default of either polarity would turn that pull into a silent withdrawal or restoration. The rights are read through a per-process cache, so withdrawing one publishes a notify_event that drops the entry on every replica. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dsf6VC4MVLisiEoeQkgbr4 * feat: enforce operator write rights on the router and in the UI Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: close the capture gap and gate the trigger editors' write actions Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: gate acl writes and the native trigger drawer behind manage rights Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: refuse operator writes with 403 and gate sharing at the drawer Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * perf: resolve identity in the operator write gate only for writes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: gate the suspended-jobs actions and stop the route check refusing reads Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: explain the empty-state create button when operator writes are withdrawn Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: audit operator settings changes and fold path writes into native rows Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: open locked editors read-only and group the operator settings Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: skip email and azure lookups on editor open while triggers are locked Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs: state each operator-rights rationale once in comments Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: address CI review findings on operator write rights Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: keep capture move gated and skip it in the builders while locked Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: keep admin and operator exclusive when setting a workspace role Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor: use the shared section component for operator settings groups Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Ruben Fiszel <ruben@windmill.dev> |
||
|
|
94e4fb1c84 |
fix: carry labels when deploying variables, resources and folders (#11222)
* fix: carry labels when deploying variables, resources and folders across workspaces Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: clear a folder's labels in the target when the source has none Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: pin that the frontend deploy adapter carries variable labels Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9b46b21f51 |
feat(cli): include extra paths in codebase digest for monorepo imports (#11327)
* feat(cli): hash codebase extra_digest_paths and warn on uncovered bundle inputs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cli): cache codebase digest only once complete, skip entry in warning Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cli): accept string extra_digest_paths, always print uncovered-input warning Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cli): clarify extra_digest_paths resolution and missing-path error Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
25c8bfaab4 |
chore(main): release 1.817.0 (#11268)
* chore(main): release 1.817.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> |
||
|
|
841f7e1436 |
fix(cli): sync workspace dependency files on Windows and fix CLI tests (#11288)
* fix(cli): sync workspace dependency files on Windows Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(cli): pin workspace dependency path parsing on both separators Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(cli): classify workspace dependency files on either separator Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(cli): check out the fork deploy clone without autocrlf Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
965e8b0c23 |
chore(main): release 1.816.0 (#11237)
* chore(main): release 1.816.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> |
||
|
|
e1e3692fbc |
feat: data table roles in the DB manager and raw apps (#11139)
* feat(datatables): put a data table's connection under Postgres roles A data table backed by the instance database resolved to exactly one Postgres connection, `custom_instance_user`, for everyone who could reach it at all. There was no way to say this job reads, that one writes, this one never sees the salaries table. A data table role is now a real Postgres login on the cluster, defined once for the instance by a superadmin and named exactly as they named it. A script that declares `-- role analytics` connects as `analytics`, and Postgres decides what it may touch — grants are ordinary SQL. Windmill answers only "may this caller ask for this role", from the tenant lists on the data table entry: `u/alice`, `g/analysts`, `f/finance` or `*`. A data table with no `permissions` block behaves exactly as before. Everything that opens a connection on someone's behalf goes through one chokepoint, `get_datatable_resource_from_db`, which takes the identity explicitly and fails closed when there is none. The role logs in as itself — never `SET ROLE`, which a script could `RESET ROLE` its way out of. A fork's data table entry becomes a pointer at the workspace that governs it rather than a copy of it. The settings clone used to hand a fork a byte-identical entry naming the parent's database, which a fork admin could edit to grant themselves `admin` there; a pointer has nothing local to edit, and its tenants are evaluated as a member of the governing workspace, by email. `permissions` is stripped from the workspace export and ignored on import: tenants name principals of one workspace, and a settings push is not where an access decision should be made. Operations that see the whole database whatever the roles grant stay with the governing workspace's admins: editing the roles, a migration that declares none, and opening a replication stream for a Postgres trigger or capture. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR * fix(datatables): gate the paths that reach a whole database as admin Auditing what still resolved through the unchecked resolver turned up three that act for a caller and hand back the admin connection: `resolve_pg_source_checked` (behind schema export, the full-schema read, database creation, import and the forked-database drop), the connection test, and the schema snapshot a fork clone takes of its parent. On a data table under roles each let any workspace member — or a fork admin who is nobody in the governing workspace — read or copy the whole database whatever its roles grant. All three now require admin reach on the governing workspace. A dump taken under a restricted role would be a silently truncated copy rather than an error, so refusing is the only right answer for the copy paths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR * fix(datatables): confine roles to the instance database, and stop a fork reaching the parent's bookkeeping A data table role is a login on Windmill's own Postgres. Nothing stopped a workspace admin putting a *resource-backed* data table under roles, at which point the executor dialled the host that resource names — one the admin chose — with the role's real cluster password, and `CONNECT` is granted to every registered instance database. Both ends now refuse: the permissions endpoint rejects the save, and the chokepoint refuses to substitute credentials on a non-instance entry rather than trusting the record it read. Two more places reached the governing database without answering to it. The initial-migration generator returned a `pg_dump` of the whole schema to any member. And the migration rename/delete cascade followed a fork's pointer into the parent, so a fork admin renaming or removing their own local entry relabelled or wiped the parent's `_wm_migrations` — after which the parent re-runs every migration from zero. The remote half is now skipped when the entry resolves into another workspace, which is also just correct: a fork renaming what it calls a data table changes nothing about the data table. Also: revoking a tenant now bounces the replication streams of every workspace holding an entry that resolves here, not only the governing one, so a fork's trigger stops rather than living on inside its open connection; the instance role catalog and the governing workspace's tenant lists are no longer returned to someone who cannot edit them; and the tenant rename dedup collapses non-adjacent duplicates, per role rather than once any role changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR * fix(datatables): fail loudly where a role or a pointer can be left half-recorded Three ways the feature could end up in a state nobody could see or undo. Creating a role writes the cluster first and the catalog second, but the catalog write was an `UPDATE` that matched nothing when the instance Postgres settings row was absent — leaving a live login with a password nobody recorded: invisible to the catalog, un-recreatable because the name is taken, and un-deletable because there is no entry to delete. It now errors, so the operation is retryable once the row is restored. Deleting a workspace only nulls the fork lineage; the data table entries pointing at it are left resolving to nothing. Sweeping them is not an option — turning a pointer back into a copy would hand each fork the database outright — so the delete now names the data tables it stranded, and resolving one says which workspace is missing rather than reporting a data table this workspace never had. `InstanceDatatableRole` derived `Debug` while holding a Postgres password; it is now hand-written so `{:?}` on the catalog cannot put a live credential in a log line. Adds the two branches the reviews found unpinned: a caller who is not a member of the governing workspace at all, and `NoIdentity` — the compatibility path for an agent worker that predates this and sends no job id. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR * fix(datatables): unbreak two operator messages and two comments that described other code The two strings this branch added for states an operator hits once — the catalog write that matched nothing, and the delete that stranded a pointer — were collapsed from their multi-line form with the indentation left in, so both rendered with a fourteen-space gap mid-sentence. `list_datatables` claimed to report a chain it cannot follow and then dropped it; it does drop it, and the comment now says why that is the right place to stay quiet. The non-superadmin check in `edit_datatable_config` was introduced as also covering references, which it does not and need not: `reference` is overwritten from the stored entry for every caller before the check runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR * fix(datatables): serialize role catalog mutations, and state each helper's authorization contract The catalog is one JSON document, so create, rename, enable and delete are all read-modify-write. Two concurrent creates read the same snapshot, both succeed in the cluster, and the second write drops the first — leaving a live Postgres login with a password nobody recorded, which is the exact state the delete path exists to prevent. Every mutation now runs in one transaction holding an advisory lock across the read, the cluster DDL and the write, so a lost update cannot happen and a failure rolls the whole thing back. The DDL helpers take that transaction rather than the pool, which is what makes the lock cover them. Their statements moved off `sqlx::raw_sql`: the simple protocol is only needed for genuinely multi-statement SQL, and its future is not `Send`, which an axum handler holding the transaction requires. Each of these is one statement anyway. The new cross-crate surface now says what callers must do. `read_role_catalog` returns plaintext credentials; `create`/`rename`/`set_login`/`drop_instance_role` and `converge_connect_grants` mutate cluster-wide state; `read_datatable_entry` reads a workspace's raw config. All of them are superadmin-gated by their current handlers, but nothing said so at the definition, which is where the next caller looks. Also: the roles table reloads after a failed login toggle instead of leaving it claiming a flip that did not land; the rename affordance is the design-system `Button`, not a raw one; and `resolve_datatable_pg_as_caller` drops a `role` parameter no caller ever filled — browsing resolves as the data table's default until the database manager grows a picker. Why role passwords stay a plain `String` while the instance user's password beside them is a `StringOrSecretRef`, asked three times across reviews: that one is a secret ref because an operator supplies it and may want it from their own backend, while these are minted here and never entered by anyone, so there is nothing for a ref to point at. Encrypting generated secrets at rest is a separate change that would take the replication password with it. Now said at the field. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR * fix(datatables): give the role catalog its own row, out of reach of the config machinery Putting it inside `custom_instance_pg_databases` was the wrong call, and it cost two ways. The catalog serializes a generated Postgres password per role, and that row is the operator-facing instance config, so the passwords reached `get_instance_config` and its YAML editor — a live cluster credential in a response body, a UI field and any log of either. Worse in the other direction: `to_settings_map` strips the catalog, so a full-row upsert of that key writes the row back without it and the catalog is gone, while the cluster keeps every login it described. `custom_instance_replication_pwd` is the precedent and says exactly why — a generated secret, written only by the server, never operator-authored, hidden so the config machinery cannot read, rewrite or drop it. The catalog is the same thing, so it now has the same shape: `datatable_roles`, in `HIDDEN_SETTINGS`, `PROTECTED_SETTINGS` and the agent-worker denylist. No redaction to keep in step with three code paths, and no way for a neighbouring write to take it out. Two races on the same shared documents. `edit_datatable_config` read the stored data tables outside its transaction and then wrote the whole `datatable` document, so a permissions save committing in between was silently rolled back; it now reads under `FOR UPDATE`. And `set_datatable_permissions` validated role ids against the catalog before opening its transaction, so a deletion in between let it write a deleted role back — including as the default, which every later job then fails on; it now holds the catalog lock and the settings row across validation and write. Completes the authorization contracts the previous commit claimed but did not finish: `read_datatable_entry` (which it named and missed), `resolve_governing_datatable`, whose whole job is to answer for a workspace the caller may not belong to, and `converge_connect_grants_with`, which had not inherited its wrapper's. Also the generic Python SDK reference: `_format_py_params` learned the bare `*` last time, but `extract_py_functions` is a second formatter and still rendered `datatable(name, role)`, so code written from that page passed a keyword-only argument positionally. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR * fix(datatables): make the concurrency test pin the handlers, and the contracts describe what is enforced The concurrency test reimplemented the read-modify-write inline, so deleting the lock from all three handlers left it green — it pinned Postgres, not the code it was written for. It now drives `create_datatable_role` twice concurrently and asserts the catalog kept both names. Checked the way the last one should have been: removing the lock from the handler makes it fail with "wmtest_a_… is a live cluster login the catalog forgot". The contracts added last commit were stricter than this PR's own callers, which is worse than none — the next reader sees a rule already broken and learns to ignore it. `read_role_catalog` said superadmin-only while two of its four callers are open to any workspace member, and `converge_connect_grants` said superadmin while `set_datatable_permissions` reaches it as a workspace admin. Both were fine on substance: the rule that actually holds is about the credential never reaching a response, log, audit record or export, not about who may call. They now say that. `read_datatable_entry` gets the same treatment rather than the one the earlier message claimed for it: it is the primitive every resolution goes through, so it is deliberately open, and what must not escape is `permissions` — it names the governing workspace's users, groups and folders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR * fix(datatables): close the last ways a role or a pointer can be left pointing at nothing The raw settings readers hand back whatever is in the row, so moving the catalog into its own `global_settings` key protected the config machinery and left `GET /settings/global/datatable_roles` and the settings listing returning every live password. Both now filter that one key. The neighbouring `custom_instance_replication_pwd` has the same shape and is not touched here: it predates this and widening the fix to it is a decision about an operator workflow, not a consequence of this change. Three ways a save could leave something resolving to nothing: A permissioned data table could be moved to a PostgreSQL resource. The block was carried across as a server-owned field, the runtime refuses roles on a resource-backed table, so the save succeeded and every job afterwards failed. Refused instead — turning roles off first is one step, and it keeps discarding an access decision something somebody chose. Renaming a governing data table left every fork pointing at the old name: the data table disappears from their pickers and their jobs stop, with nothing in the renaming workspace to suggest why. The rename now follows into the pointers in the same transaction. Deleting one cannot be followed the same way, so it is reported instead — the response names what it stranded, the way deleting a workspace does, and the fork's own error already says which workspace is gone. Also: `ensure_instance_db_grant_options_unchecked` claimed superadmin while the permissions handler reaches it as a workspace admin (the same class fixed last commit, one instance missed); the role entry kept an `instance_config_schema` derive it no longer needs; `write_role_catalog` was the one writer of that table not stamping `updated_at`; and the concurrency test dropped its roles only on success — a failing run is exactly the one that creates them without recording them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR * refactor(datatables): put the role catalog in its own table, not in global_settings Five findings across three rounds were all the same choice. A set of live Postgres credentials was living in `global_settings`, which has generic read, list, write, config-export and CLI round-trip paths that know nothing about what they carry: the passwords reached the instance config and its YAML editor, a full-row upsert of a neighbouring key erased the catalog, `GET /settings/global/{key}` and the settings listing returned them raw, and this round the redaction that fixed the last two turned `wmill instance push` into something that wipes every password — a fix breaking the assumption the previous fix made. `POST /settings/global/datatable_roles` could also empty it outside the lock. The approved plan offered a table or `global_settings`, so this is the other option it already allowed rather than a new design. `datatable_role` is a table: no generic settings path can read it, list it, export it, write it or round-trip it, so none of the five needs a guard. The redaction, the hidden/protected/agent-denylist entries and the JSON document all go with it. One row per role also removes the read-modify-write the concurrency work was about: two concurrent creates are two inserts, and the unique index on `name` is what settles a collision. The advisory lock stays for the one window rows do not cover — `CREATE ROLE` is invisible to another transaction until commit, so without it both creates pass their `pg_roles` check. Also from this round: rename mappings are checked against the configuration they claim to describe, since fork pointers are rewritten from them — a caller could otherwise submit `main -> missing` against an unchanged config and repoint every fork of `main` at a name nothing has, and `A -> B` plus `B -> C` moved what pointed at `A` all the way to `C`. And the warning naming forks a delete stranded reached the response but not the screen: both the data table settings save and the workspace delete now show it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR * fix(datatables): validate a rename against the save it describes, and re-check under the locks Three from the round, all about deciding on state that could already have moved. A permission save resolved the data table and checked it was instance-backed before taking any lock, then wrote under one. A config save committing in between could move the table onto a PostgreSQL resource — recreating exactly what the transition guard refuses — or rename it, in which case the write targeted a key that no longer existed and reported success having changed nothing. It now re-resolves and re-checks on the locked state. Rename validation checked that the source existed before and the target existed after, which still accepts `main -> decoy` against a save that keeps both: every fork of `main` then follows onto a different data table, silently, because it keeps resolving. The rule is now the actual old-to-new key transition — a source may only survive if another rename took its name, and a target may only pre-exist if another rename freed it. That also stops two sources sharing one target, and it admits a swap, which the previous guard refused: `datatables` is keyed by name, so a swap cannot be done one save at a time, and refusing it was a regression against main. The pointer cascade now runs in two passes through a temporary name, the way the migration cascade one layer down already handles the same shape, so `A -> B` with `B -> C` moves each pointer once from what it named before the save. The tenant mutators say what they are for: they write an access decision for any workspace named, with an arbitrary mutation, and exist for the transaction that frees or renames a principal. Editing a decision on purpose belongs in the permissions endpoint. Carried in the same change: the stranded-fork list is a field rather than a phrase to grep out of a success string; the pointer cascade matches with `EXISTS` instead of a `LIKE` over the whole document, so a workspace whose pointers name something else is not rewritten to a byte-identical value under an exclusive lock; and `InstanceDatatableRole` drops the serde derives left over from the JSON document, one of which would emit `pwd`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR * fix(datatables): cascade on the leave route that is used, gate migrations before the admin connection, and drop a role atomically The tenant cascade on leaving went onto `/users/leave`. The UI and the generated client call `/workspaces/leave` — a different handler in a different crate with the same name — which deleted the membership and left `u/<username>` in the tenant lists. Leaving and rejoining therefore restored the access the leave was supposed to end, and a later account taking the username would have inherited it. The regression test drives the route the client actually calls; without the fix it fails with "leaving kept the tenant". The migration endpoints authorized too late. `run_datatable_migrations` opened the data table's admin connection, created `_wm_migrations` and read it before reaching the per-migration role check — so with nothing pending, nothing was checked at all. Rollback returned before its check when nothing was applied, and the status endpoint had none. All three now ask, before any connection is opened, whether the caller can reach the data table as any role at all; which role a given migration runs as is still decided per migration, and by the executor after that. Deleting a role committed the cluster drop and the catalog row, then swept the tenant lists in separate transactions. A sweep failing part-way left workspaces naming a role nothing can connect as, while the retry answered `NotFound` because the catalog entry was already gone. The sweep now runs in the same transaction, so the drop, the row and every tenant list commit together. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR * fix(datatables): refuse to copy a data table that is under roles pg_dump carries no roles and the import runs with --no-privileges, so a copied data table arrives owned by the admin connection with no GRANT for any role. The settings clone brings `permissions` across, so the fork's tenants pass Windmill's check, connect as the role they were given, and are denied by Postgres on everything: an entry that reads as configured and answers nothing. Refuse the copy — in the import endpoint before any data moves, and in the fork path the CLI takes. Replaying the source's owners and ACLs into the clone is what lifts this, and is a change of its own. Dropping `permissions` from the copy instead would be the unsafe half, since the copy holds the parent's rows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR * fix(datatables): refuse the clone's database too, not only its data A clone is two endpoints: `create_pg_database` then `import_pg_database`. Only the second refused a data table under roles, so a fork asking to clone one created and registered an empty `wm_fork_…` instance database and then failed — and nothing collects it, since `drop_forked_datatable_databases` only drops entries carrying `forked_from` and no entry names this one. Refuse in both, so the clone stops before a database exists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR * nit worker error msg * fix pg_dump stuck on version 17 on nix * fix(datatables): refuse a malformed role annotation instead of ignoring it `-- Role operator`, `-- role operator;` and `-- role operator -- why` all failed the annotation parser's exact-match rule, so the query fell through to the data table's default role and ran, silently, under a login the author did not choose. Naming a role exists precisely to not do that. A leading comment whose first word is `role` is now an annotation attempt: the keyword matches case-insensitively, one trailing `;` is tolerated, and anything else is an error naming the line. Only callers that already know the target is a `datatable://` reference ever run this, so ordinary SQL keeps its comments. Also bumps the dev shell's postgres client to 18 — it trailed the server the dev database runs, which takes out every data table export, clone and fork-with-data. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR * fix(datatables): refuse a malformed role query string instead of ignoring it `?Role=analytics`, `?role=` and `?x=1&role=…` all fell through the reference parser's exact-match rule, so the connection resolved to the data table's default role and ran under a login the caller never asked for — the URI half of the same trap as a malformed `-- role` annotation. The key now matches case-insensitively, and anything else in the query string is an error naming it; `role` is the only parameter a reference takes. Callers that only need the entry keep a lenient `datatable_ref_name`, since they never act on the role. The DuckDB `ATTACH` parser propagates it rather than attaching under the default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR * fix(datatables): carry the role annotation into the row_to_json retry The retry rebuilds its SQL from `pruneComments(code)`, so the leading comment block never reached the second attempt — and with it the `-- role <name>` line that decides which login the query runs as. The retry connected as the data table's default role instead, so a query the first attempt was denied could succeed on the second, reported as "recovered with the row_to_json fix". Carry the leading comment block over. The retry itself is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BjfMkJyKzodxkobqGZ6Lqb * chore(datatables): don't mount the roles UI until the ACL editor lands Enforcement ships first. The permissions drawer is what turns roles on, and the catalog section is what creates them — both are only useful once there is a way to grant a role the privileges it needs, which arrives with the ACL editor. Left mounted they would offer a feature whose other half does not exist. The two components are complete and reviewed; only their call sites here are commented out, with a note pointing the follow-up PRs at them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BjfMkJyKzodxkobqGZ6Lqb * fix(datatables): honour `-- role: x`, and fix the DuckDB attach test Two review findings, both real. `attach_datatable_parses_name_and_role` never compiled: `parse_attach_datatable` returns `Result<Option<_>>` now and one call site kept a single `unwrap`. Its `?Role=analytics` case also asserted a refusal, contradicting the parser in the same commit, which matches the key case-insensitively. Replaced with the cases that are genuinely malformed, and a positive one for the cased key. `-- role: analytics` fell through to the default role — the silent fallback the strict parser exists to remove, for the spelling most likely to be typed. The keyword now accepts an optional colon, attached or spaced, while a word that merely starts with it (`rolebased`) is still not an attempt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BjfMkJyKzodxkobqGZ6Lqb * fix(datatables): clone a fork's pointer instead of failing after the copy Forking a fork with cloning left an orphan database. The preflight resolves the pointer and sees the governing entry, so both endpoints ran and filled the new database; `apply_forked_datatable` then refused the inherited pointer and rolled the fork back, stranding a registered `wm_fork_*` that no entry names and whose name blocks the retry. Refusing earlier would have been the smaller change, but forking a fork and cloning worked before pointers existed, so it would trade an orphan for a regression. Resolve what the pointer names and write the terminal entry the clone needs: the whole `database` object rather than a patch of its `resource_path`, since a pointer has none, and `reference` removed with it. Also accepts `-- role=x` and `-- Role = x`, two more spellings that fell through to the default role. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BjfMkJyKzodxkobqGZ6Lqb * fix(datatables): refuse to roll back the catalog while roles exist The down migration dropped the table and left every role behind: live Postgres logins whose passwords only that table carried, so after a revert Windmill could neither use, disable nor delete them, and re-applying could not recreate them because the names were taken. Cleaning up here is not possible either — dropping a role means reassigning what it owns in every instance database, and a migration runs in one — so it now refuses while the catalog is non-empty and says to delete the roles through instance settings, which does the cluster work. Also enforces the instance-only invariant the resolved-pointer clone relies on rather than only asserting it in a comment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BjfMkJyKzodxkobqGZ6Lqb * refactor(datatables): settle clonability in one place, before anything is created A clone is three stages a workspace apart — `create_pg_database`, then `import_pg_database`, then `apply_forked_datatable` inside the fork transaction. Only the third can roll back, and `CREATE DATABASE` is not transactional, so any refusal that lives there strands a registered `wm_fork_*` that no entry names and whose name blocks the retry. That orphan has now been fixed three times, most recently reintroduced by a guard added one commit ago. Patching each new refusal into the first endpoint is not the fix; having two places that can refuse is. `ensure_datatable_is_clonable` now answers every reason a copy can be refused and returns what it resolved, and the stage that writes the entry only does the work. Also takes an ACCESS EXCLUSIVE lock before the rollback guard counts, so a role created concurrently cannot slip between the check and the drop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BjfMkJyKzodxkobqGZ6Lqb * fix(datatables): let a retried clone reclaim its own leftover database A clone creates its target database one request before it copies into it, and the fork that would name it is written a request after that. Any failure in between — a pg_dump error, a bad restore, a dropped connection, the source's roles changing mid-flow — left a registered `wm_fork_*` that no entry names, and every retry then failed on its name. This predates data table roles. `create_pg_database` now reclaims such a leftover before creating: only a `wm_fork_*` database Windmill registered as a data table database and that no data table or ducklake entry names, in any workspace, archived ones included. The drop never terminates connections, so a clone still copying into it makes the reclaim fail instead of being cut off. It is limited to callers who administer the source — reaching it is not enough, since on a data table without roles every member reaches it — and anyone else gets the refusal an existing database always got. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Revert "fix(datatables): let a retried clone reclaim its own leftover database" This reverts commit |
||
|
|
974b0265f8 |
fix(cli): read wmill.yaml from the branch a git-sync deploy writes to (#11236)
* fix(cli): read wmill.yaml from the branch a git-sync deploy writes to Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(cli): create the stateful dir once the deploy branch's config is read Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
01b38320ea |
chore(main): release 1.815.0 (#11212)
* chore(main): release 1.815.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> |
||
|
|
de4818d905 |
chore(security): resolve Dependabot alerts in frontend, windmill-yaml-validator, typescript-client and cli lockfiles (#11181)
Targeted dependency bumps only; no behavior change intended. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
df61dea5fa |
fix: keep instance groups when editing auto-invite (#11217)
* fix: keep instance groups when editing auto-invite and push them from sync Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: compare settings.yaml without undeclared instance groups on push Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: read a missing auto_invite as empty when diffing settings.yaml on push Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore: update ee-repo-ref to f2fced19fcae81de7f6dac545010ce404c052e1b This commit updates the EE repository reference after PR #813 was merged in windmill-ee-private. Previous ee-repo-ref: cf4258c1232720ca3b82db2b22ea1fb4bca9533e New ee-repo-ref: f2fced19fcae81de7f6dac545010ce404c052e1b 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> |
||
|
|
0e807fb1dd |
feat: put a data table's connection under Postgres roles (#11020)
* feat(datatables): put a data table's connection under Postgres roles
A data table backed by the instance database resolved to exactly one Postgres connection,
`custom_instance_user`, for everyone who could reach it at all. There was no way to say
this job reads, that one writes, this one never sees the salaries table.
A data table role is now a real Postgres login on the cluster, defined once for the
instance by a superadmin and named exactly as they named it. A script that declares
`-- role analytics` connects as `analytics`, and Postgres decides what it may touch —
grants are ordinary SQL. Windmill answers only "may this caller ask for this role", from
the tenant lists on the data table entry: `u/alice`, `g/analysts`, `f/finance` or `*`.
A data table with no `permissions` block behaves exactly as before.
Everything that opens a connection on someone's behalf goes through one chokepoint,
`get_datatable_resource_from_db`, which takes the identity explicitly and fails closed when
there is none. The role logs in as itself — never `SET ROLE`, which a script could
`RESET ROLE` its way out of.
A fork's data table entry becomes a pointer at the workspace that governs it rather than a
copy of it. The settings clone used to hand a fork a byte-identical entry naming the
parent's database, which a fork admin could edit to grant themselves `admin` there; a
pointer has nothing local to edit, and its tenants are evaluated as a member of the
governing workspace, by email. `permissions` is stripped from the workspace export and
ignored on import: tenants name principals of one workspace, and a settings push is not
where an access decision should be made.
Operations that see the whole database whatever the roles grant stay with the governing
workspace's admins: editing the roles, a migration that declares none, and opening a
replication stream for a Postgres trigger or capture.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR
* fix(datatables): gate the paths that reach a whole database as admin
Auditing what still resolved through the unchecked resolver turned up three that act for a
caller and hand back the admin connection: `resolve_pg_source_checked` (behind schema
export, the full-schema read, database creation, import and the forked-database drop), the
connection test, and the schema snapshot a fork clone takes of its parent. On a data table
under roles each let any workspace member — or a fork admin who is nobody in the governing
workspace — read or copy the whole database whatever its roles grant.
All three now require admin reach on the governing workspace. A dump taken under a
restricted role would be a silently truncated copy rather than an error, so refusing is the
only right answer for the copy paths.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR
* fix(datatables): confine roles to the instance database, and stop a fork reaching the parent's bookkeeping
A data table role is a login on Windmill's own Postgres. Nothing stopped a workspace admin
putting a *resource-backed* data table under roles, at which point the executor dialled the
host that resource names — one the admin chose — with the role's real cluster password, and
`CONNECT` is granted to every registered instance database. Both ends now refuse: the
permissions endpoint rejects the save, and the chokepoint refuses to substitute credentials
on a non-instance entry rather than trusting the record it read.
Two more places reached the governing database without answering to it. The initial-migration
generator returned a `pg_dump` of the whole schema to any member. And the migration
rename/delete cascade followed a fork's pointer into the parent, so a fork admin renaming or
removing their own local entry relabelled or wiped the parent's `_wm_migrations` — after
which the parent re-runs every migration from zero. The remote half is now skipped when the
entry resolves into another workspace, which is also just correct: a fork renaming what it
calls a data table changes nothing about the data table.
Also: revoking a tenant now bounces the replication streams of every workspace holding an
entry that resolves here, not only the governing one, so a fork's trigger stops rather than
living on inside its open connection; the instance role catalog and the governing workspace's
tenant lists are no longer returned to someone who cannot edit them; and the tenant rename
dedup collapses non-adjacent duplicates, per role rather than once any role changed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR
* fix(datatables): fail loudly where a role or a pointer can be left half-recorded
Three ways the feature could end up in a state nobody could see or undo.
Creating a role writes the cluster first and the catalog second, but the catalog write was an
`UPDATE` that matched nothing when the instance Postgres settings row was absent — leaving a
live login with a password nobody recorded: invisible to the catalog, un-recreatable because
the name is taken, and un-deletable because there is no entry to delete. It now errors, so
the operation is retryable once the row is restored.
Deleting a workspace only nulls the fork lineage; the data table entries pointing at it are
left resolving to nothing. Sweeping them is not an option — turning a pointer back into a copy
would hand each fork the database outright — so the delete now names the data tables it
stranded, and resolving one says which workspace is missing rather than reporting a data table
this workspace never had.
`InstanceDatatableRole` derived `Debug` while holding a Postgres password; it is now
hand-written so `{:?}` on the catalog cannot put a live credential in a log line.
Adds the two branches the reviews found unpinned: a caller who is not a member of the
governing workspace at all, and `NoIdentity` — the compatibility path for an agent worker that
predates this and sends no job id.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR
* fix(datatables): unbreak two operator messages and two comments that described other code
The two strings this branch added for states an operator hits once — the catalog write that
matched nothing, and the delete that stranded a pointer — were collapsed from their multi-line
form with the indentation left in, so both rendered with a fourteen-space gap mid-sentence.
`list_datatables` claimed to report a chain it cannot follow and then dropped it; it does drop
it, and the comment now says why that is the right place to stay quiet. The non-superadmin
check in `edit_datatable_config` was introduced as also covering references, which it does not
and need not: `reference` is overwritten from the stored entry for every caller before the
check runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR
* fix(datatables): serialize role catalog mutations, and state each helper's authorization contract
The catalog is one JSON document, so create, rename, enable and delete are all
read-modify-write. Two concurrent creates read the same snapshot, both succeed in the
cluster, and the second write drops the first — leaving a live Postgres login with a password
nobody recorded, which is the exact state the delete path exists to prevent. Every mutation
now runs in one transaction holding an advisory lock across the read, the cluster DDL and the
write, so a lost update cannot happen and a failure rolls the whole thing back. The DDL
helpers take that transaction rather than the pool, which is what makes the lock cover them.
Their statements moved off `sqlx::raw_sql`: the simple protocol is only needed for genuinely
multi-statement SQL, and its future is not `Send`, which an axum handler holding the
transaction requires. Each of these is one statement anyway.
The new cross-crate surface now says what callers must do. `read_role_catalog` returns
plaintext credentials; `create`/`rename`/`set_login`/`drop_instance_role` and
`converge_connect_grants` mutate cluster-wide state; `read_datatable_entry` reads a workspace's
raw config. All of them are superadmin-gated by their current handlers, but nothing said so at
the definition, which is where the next caller looks.
Also: the roles table reloads after a failed login toggle instead of leaving it claiming a flip
that did not land; the rename affordance is the design-system `Button`, not a raw one; and
`resolve_datatable_pg_as_caller` drops a `role` parameter no caller ever filled — browsing
resolves as the data table's default until the database manager grows a picker.
Why role passwords stay a plain `String` while the instance user's password beside them is a
`StringOrSecretRef`, asked three times across reviews: that one is a secret ref because an
operator supplies it and may want it from their own backend, while these are minted here and
never entered by anyone, so there is nothing for a ref to point at. Encrypting generated
secrets at rest is a separate change that would take the replication password with it. Now
said at the field.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR
* fix(datatables): give the role catalog its own row, out of reach of the config machinery
Putting it inside `custom_instance_pg_databases` was the wrong call, and it cost two ways.
The catalog serializes a generated Postgres password per role, and that row is the
operator-facing instance config, so the passwords reached `get_instance_config` and its YAML
editor — a live cluster credential in a response body, a UI field and any log of either.
Worse in the other direction: `to_settings_map` strips the catalog, so a full-row upsert of
that key writes the row back without it and the catalog is gone, while the cluster keeps every
login it described.
`custom_instance_replication_pwd` is the precedent and says exactly why — a generated secret,
written only by the server, never operator-authored, hidden so the config machinery cannot
read, rewrite or drop it. The catalog is the same thing, so it now has the same shape:
`datatable_roles`, in `HIDDEN_SETTINGS`, `PROTECTED_SETTINGS` and the agent-worker denylist.
No redaction to keep in step with three code paths, and no way for a neighbouring write to
take it out.
Two races on the same shared documents. `edit_datatable_config` read the stored data tables
outside its transaction and then wrote the whole `datatable` document, so a permissions save
committing in between was silently rolled back; it now reads under `FOR UPDATE`. And
`set_datatable_permissions` validated role ids against the catalog before opening its
transaction, so a deletion in between let it write a deleted role back — including as the
default, which every later job then fails on; it now holds the catalog lock and the settings
row across validation and write.
Completes the authorization contracts the previous commit claimed but did not finish:
`read_datatable_entry` (which it named and missed), `resolve_governing_datatable`, whose whole
job is to answer for a workspace the caller may not belong to, and
`converge_connect_grants_with`, which had not inherited its wrapper's.
Also the generic Python SDK reference: `_format_py_params` learned the bare `*` last time, but
`extract_py_functions` is a second formatter and still rendered `datatable(name, role)`, so
code written from that page passed a keyword-only argument positionally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR
* fix(datatables): make the concurrency test pin the handlers, and the contracts describe what is enforced
The concurrency test reimplemented the read-modify-write inline, so deleting the lock from all
three handlers left it green — it pinned Postgres, not the code it was written for. It now
drives `create_datatable_role` twice concurrently and asserts the catalog kept both names.
Checked the way the last one should have been: removing the lock from the handler makes it
fail with "wmtest_a_… is a live cluster login the catalog forgot".
The contracts added last commit were stricter than this PR's own callers, which is worse than
none — the next reader sees a rule already broken and learns to ignore it.
`read_role_catalog` said superadmin-only while two of its four callers are open to any
workspace member, and `converge_connect_grants` said superadmin while
`set_datatable_permissions` reaches it as a workspace admin. Both were fine on substance: the
rule that actually holds is about the credential never reaching a response, log, audit record
or export, not about who may call. They now say that. `read_datatable_entry` gets the same
treatment rather than the one the earlier message claimed for it: it is the primitive every
resolution goes through, so it is deliberately open, and what must not escape is `permissions`
— it names the governing workspace's users, groups and folders.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR
* fix(datatables): close the last ways a role or a pointer can be left pointing at nothing
The raw settings readers hand back whatever is in the row, so moving the catalog into its own
`global_settings` key protected the config machinery and left `GET /settings/global/datatable_roles`
and the settings listing returning every live password. Both now filter that one key. The
neighbouring `custom_instance_replication_pwd` has the same shape and is not touched here: it
predates this and widening the fix to it is a decision about an operator workflow, not a
consequence of this change.
Three ways a save could leave something resolving to nothing:
A permissioned data table could be moved to a PostgreSQL resource. The block was carried across
as a server-owned field, the runtime refuses roles on a resource-backed table, so the save
succeeded and every job afterwards failed. Refused instead — turning roles off first is one step,
and it keeps discarding an access decision something somebody chose.
Renaming a governing data table left every fork pointing at the old name: the data table
disappears from their pickers and their jobs stop, with nothing in the renaming workspace to
suggest why. The rename now follows into the pointers in the same transaction.
Deleting one cannot be followed the same way, so it is reported instead — the response names what
it stranded, the way deleting a workspace does, and the fork's own error already says which
workspace is gone.
Also: `ensure_instance_db_grant_options_unchecked` claimed superadmin while the permissions
handler reaches it as a workspace admin (the same class fixed last commit, one instance missed);
the role entry kept an `instance_config_schema` derive it no longer needs; `write_role_catalog`
was the one writer of that table not stamping `updated_at`; and the concurrency test dropped its
roles only on success — a failing run is exactly the one that creates them without recording them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR
* refactor(datatables): put the role catalog in its own table, not in global_settings
Five findings across three rounds were all the same choice. A set of live Postgres credentials
was living in `global_settings`, which has generic read, list, write, config-export and CLI
round-trip paths that know nothing about what they carry: the passwords reached the instance
config and its YAML editor, a full-row upsert of a neighbouring key erased the catalog,
`GET /settings/global/{key}` and the settings listing returned them raw, and this round the
redaction that fixed the last two turned `wmill instance push` into something that wipes every
password — a fix breaking the assumption the previous fix made. `POST /settings/global/datatable_roles`
could also empty it outside the lock.
The approved plan offered a table or `global_settings`, so this is the other option it already
allowed rather than a new design. `datatable_role` is a table: no generic settings path can read
it, list it, export it, write it or round-trip it, so none of the five needs a guard. The
redaction, the hidden/protected/agent-denylist entries and the JSON document all go with it.
One row per role also removes the read-modify-write the concurrency work was about: two
concurrent creates are two inserts, and the unique index on `name` is what settles a collision.
The advisory lock stays for the one window rows do not cover — `CREATE ROLE` is invisible to
another transaction until commit, so without it both creates pass their `pg_roles` check.
Also from this round: rename mappings are checked against the configuration they claim to
describe, since fork pointers are rewritten from them — a caller could otherwise submit
`main -> missing` against an unchanged config and repoint every fork of `main` at a name nothing
has, and `A -> B` plus `B -> C` moved what pointed at `A` all the way to `C`. And the warning
naming forks a delete stranded reached the response but not the screen: both the data table
settings save and the workspace delete now show it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR
* fix(datatables): validate a rename against the save it describes, and re-check under the locks
Three from the round, all about deciding on state that could already have moved.
A permission save resolved the data table and checked it was instance-backed before taking any
lock, then wrote under one. A config save committing in between could move the table onto a
PostgreSQL resource — recreating exactly what the transition guard refuses — or rename it, in
which case the write targeted a key that no longer existed and reported success having changed
nothing. It now re-resolves and re-checks on the locked state.
Rename validation checked that the source existed before and the target existed after, which
still accepts `main -> decoy` against a save that keeps both: every fork of `main` then follows
onto a different data table, silently, because it keeps resolving. The rule is now the actual
old-to-new key transition — a source may only survive if another rename took its name, and a
target may only pre-exist if another rename freed it. That also stops two sources sharing one
target, and it admits a swap, which the previous guard refused: `datatables` is keyed by name, so
a swap cannot be done one save at a time, and refusing it was a regression against main. The
pointer cascade now runs in two passes through a temporary name, the way the migration cascade
one layer down already handles the same shape, so `A -> B` with `B -> C` moves each pointer once
from what it named before the save.
The tenant mutators say what they are for: they write an access decision for any workspace named,
with an arbitrary mutation, and exist for the transaction that frees or renames a principal.
Editing a decision on purpose belongs in the permissions endpoint.
Carried in the same change: the stranded-fork list is a field rather than a phrase to grep out of
a success string; the pointer cascade matches with `EXISTS` instead of a `LIKE` over the whole
document, so a workspace whose pointers name something else is not rewritten to a byte-identical
value under an exclusive lock; and `InstanceDatatableRole` drops the serde derives left over from
the JSON document, one of which would emit `pwd`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR
* fix(datatables): cascade on the leave route that is used, gate migrations before the admin connection, and drop a role atomically
The tenant cascade on leaving went onto `/users/leave`. The UI and the generated client call
`/workspaces/leave` — a different handler in a different crate with the same name — which
deleted the membership and left `u/<username>` in the tenant lists. Leaving and rejoining
therefore restored the access the leave was supposed to end, and a later account taking the
username would have inherited it. The regression test drives the route the client actually
calls; without the fix it fails with "leaving kept the tenant".
The migration endpoints authorized too late. `run_datatable_migrations` opened the data table's
admin connection, created `_wm_migrations` and read it before reaching the per-migration role
check — so with nothing pending, nothing was checked at all. Rollback returned before its check
when nothing was applied, and the status endpoint had none. All three now ask, before any
connection is opened, whether the caller can reach the data table as any role at all; which role
a given migration runs as is still decided per migration, and by the executor after that.
Deleting a role committed the cluster drop and the catalog row, then swept the tenant lists in
separate transactions. A sweep failing part-way left workspaces naming a role nothing can connect
as, while the retry answered `NotFound` because the catalog entry was already gone. The sweep now
runs in the same transaction, so the drop, the row and every tenant list commit together.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR
* fix(datatables): refuse to copy a data table that is under roles
pg_dump carries no roles and the import runs with --no-privileges, so a copied
data table arrives owned by the admin connection with no GRANT for any role.
The settings clone brings `permissions` across, so the fork's tenants pass
Windmill's check, connect as the role they were given, and are denied by
Postgres on everything: an entry that reads as configured and answers nothing.
Refuse the copy — in the import endpoint before any data moves, and in the fork
path the CLI takes. Replaying the source's owners and ACLs into the clone is
what lifts this, and is a change of its own. Dropping `permissions` from the
copy instead would be the unsafe half, since the copy holds the parent's rows.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR
* fix(datatables): refuse the clone's database too, not only its data
A clone is two endpoints: `create_pg_database` then `import_pg_database`. Only
the second refused a data table under roles, so a fork asking to clone one
created and registered an empty `wm_fork_…` instance database and then failed —
and nothing collects it, since `drop_forked_datatable_databases` only drops
entries carrying `forked_from` and no entry names this one.
Refuse in both, so the clone stops before a database exists.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR
* nit worker error msg
* fix pg_dump stuck on version 17 on nix
* fix(datatables): refuse a malformed role annotation instead of ignoring it
`-- Role operator`, `-- role operator;` and `-- role operator -- why` all failed
the annotation parser's exact-match rule, so the query fell through to the data
table's default role and ran, silently, under a login the author did not choose.
Naming a role exists precisely to not do that.
A leading comment whose first word is `role` is now an annotation attempt: the
keyword matches case-insensitively, one trailing `;` is tolerated, and anything
else is an error naming the line. Only callers that already know the target is a
`datatable://` reference ever run this, so ordinary SQL keeps its comments.
Also bumps the dev shell's postgres client to 18 — it trailed the server the dev
database runs, which takes out every data table export, clone and fork-with-data.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR
* fix(datatables): refuse a malformed role query string instead of ignoring it
`?Role=analytics`, `?role=` and `?x=1&role=…` all fell through the reference
parser's exact-match rule, so the connection resolved to the data table's default
role and ran under a login the caller never asked for — the URI half of the same
trap as a malformed `-- role` annotation.
The key now matches case-insensitively, and anything else in the query string is
an error naming it; `role` is the only parameter a reference takes. Callers that
only need the entry keep a lenient `datatable_ref_name`, since they never act on
the role. The DuckDB `ATTACH` parser propagates it rather than attaching under
the default.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ti5HyeTikPMYyW8YSdiHR
* fix(datatables): carry the role annotation into the row_to_json retry
The retry rebuilds its SQL from `pruneComments(code)`, so the leading comment
block never reached the second attempt — and with it the `-- role <name>` line
that decides which login the query runs as. The retry connected as the data
table's default role instead, so a query the first attempt was denied could
succeed on the second, reported as "recovered with the row_to_json fix".
Carry the leading comment block over. The retry itself is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BjfMkJyKzodxkobqGZ6Lqb
* chore(datatables): don't mount the roles UI until the ACL editor lands
Enforcement ships first. The permissions drawer is what turns roles on, and the
catalog section is what creates them — both are only useful once there is a way
to grant a role the privileges it needs, which arrives with the ACL editor. Left
mounted they would offer a feature whose other half does not exist.
The two components are complete and reviewed; only their call sites here are
commented out, with a note pointing the follow-up PRs at them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BjfMkJyKzodxkobqGZ6Lqb
* fix(datatables): honour `-- role: x`, and fix the DuckDB attach test
Two review findings, both real.
`attach_datatable_parses_name_and_role` never compiled: `parse_attach_datatable`
returns `Result<Option<_>>` now and one call site kept a single `unwrap`. Its
`?Role=analytics` case also asserted a refusal, contradicting the parser in the
same commit, which matches the key case-insensitively. Replaced with the cases
that are genuinely malformed, and a positive one for the cased key.
`-- role: analytics` fell through to the default role — the silent fallback the
strict parser exists to remove, for the spelling most likely to be typed. The
keyword now accepts an optional colon, attached or spaced, while a word that
merely starts with it (`rolebased`) is still not an attempt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BjfMkJyKzodxkobqGZ6Lqb
* fix(datatables): clone a fork's pointer instead of failing after the copy
Forking a fork with cloning left an orphan database. The preflight resolves the
pointer and sees the governing entry, so both endpoints ran and filled the new
database; `apply_forked_datatable` then refused the inherited pointer and rolled
the fork back, stranding a registered `wm_fork_*` that no entry names and whose
name blocks the retry.
Refusing earlier would have been the smaller change, but forking a fork and
cloning worked before pointers existed, so it would trade an orphan for a
regression. Resolve what the pointer names and write the terminal entry the
clone needs: the whole `database` object rather than a patch of its
`resource_path`, since a pointer has none, and `reference` removed with it.
Also accepts `-- role=x` and `-- Role = x`, two more spellings that fell through
to the default role.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BjfMkJyKzodxkobqGZ6Lqb
* fix(datatables): refuse to roll back the catalog while roles exist
The down migration dropped the table and left every role behind: live Postgres
logins whose passwords only that table carried, so after a revert Windmill could
neither use, disable nor delete them, and re-applying could not recreate them
because the names were taken. Cleaning up here is not possible either — dropping
a role means reassigning what it owns in every instance database, and a
migration runs in one — so it now refuses while the catalog is non-empty and
says to delete the roles through instance settings, which does the cluster work.
Also enforces the instance-only invariant the resolved-pointer clone relies on
rather than only asserting it in a comment.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BjfMkJyKzodxkobqGZ6Lqb
* refactor(datatables): settle clonability in one place, before anything is created
A clone is three stages a workspace apart — `create_pg_database`, then
`import_pg_database`, then `apply_forked_datatable` inside the fork transaction.
Only the third can roll back, and `CREATE DATABASE` is not transactional, so any
refusal that lives there strands a registered `wm_fork_*` that no entry names
and whose name blocks the retry.
That orphan has now been fixed three times, most recently reintroduced by a
guard added one commit ago. Patching each new refusal into the first endpoint is
not the fix; having two places that can refuse is. `ensure_datatable_is_clonable`
now answers every reason a copy can be refused and returns what it resolved, and
the stage that writes the entry only does the work.
Also takes an ACCESS EXCLUSIVE lock before the rollback guard counts, so a role
created concurrently cannot slip between the check and the drop.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BjfMkJyKzodxkobqGZ6Lqb
* fix(datatables): let a retried clone reclaim its own leftover database
A clone creates its target database one request before it copies into it, and
the fork that would name it is written a request after that. Any failure in
between — a pg_dump error, a bad restore, a dropped connection, the source's
roles changing mid-flow — left a registered `wm_fork_*` that no entry names,
and every retry then failed on its name. This predates data table roles.
`create_pg_database` now reclaims such a leftover before creating: only a
`wm_fork_*` database Windmill registered as a data table database and that no
data table or ducklake entry names, in any workspace, archived ones included.
The drop never terminates connections, so a clone still copying into it makes
the reclaim fail instead of being cut off. It is limited to callers who
administer the source — reaching it is not enough, since on a data table
without roles every member reaches it — and anyone else gets the refusal an
existing database always got.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Revert "fix(datatables): let a retried clone reclaim its own leftover database"
This reverts commit
|
||
|
|
d440ebb520 |
chore(main): release 1.814.0 (#11171)
* chore(main): release 1.814.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> |
||
|
|
c297ed0052 |
feat: managed memory with an inherited or custom memory id per step (#11118)
* feat: split ai agent memory into agent policy, run memory id and step history * fix: scope string memory ids to workspace and flow, keep nested tool history inputs * chore: update sqlx cache for the flow context query * docs: describe memory id scoping as collision-free rather than isolated * chore: regenerate openflow json after merging main * fix: offer no memory id for legacy manual memory, document linked history inputs * fix: seed provided messages from legacy manual memory and hide its note once set * fix: bypass memory when a provided messages expression evaluates to null * fix: require a user message when provided messages are empty * chore: keep the empty messages comment within the line width * docs: name the history inputs wherever linked steps list their flow-local inputs * docs: keep the memory storage path on one line * feat: managed memory with an inherited or custom memory id per step Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: list a custom memory id in the test run form and name where an inherited one comes from Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: keep memory id out of the add-field menu and drop the memory id telemetry Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: keep legacy auto memory without an id working after an untouched redeploy Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: rename step messages to previous_messages and address review Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * style: rewrap comments and docs lines lengthened by the previous_messages rename Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor: read agent memory as either a legacy shape or the current one Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: name the memory setting in ignored-input notes and keep conversions honest Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: keep a legacy memory count unset on open and read a cleared count as off Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: address review on cleared test history and zero-count memory Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: drop flow-local keys from a linked agent resource before interpolating it Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: keep a linked resource's own inputs as fallbacks and note ignored history on image runs Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: restore the linked agent draft tests and log ignored history on every image run Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: resolve the one-of variant from the value when the selected one leaves the list Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: treat zero-count managed memory as off when enabling chat mode and shorten comments Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: stop requiring user_message in the openflow agent contract when previous messages are the prompt Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
189793c2e4 |
feat: flow chat model picker on a shared model-settings component (#11187)
* refactor: render the session chat model menu from a shared ChatModelSettings config Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat: pick the flow chat's model and thinking from the provider fields the flow exposes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: name only the thinking level the flow run will send on the model button Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: let the flow chat take a typed model id and keep a shared thinking input editable Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: promote a flow input to the model button only where its control can edit it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: drop any reasoning token the chosen model rejects before a flow chat run Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
68f2248018 |
feat: keep flow inputs and seed the agent when chat mode is enabled (#11177)
* feat: keep flow inputs and seed the agent when chat mode is enabled Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: leave a linked agent's memory and streaming to the agent when enabling chat mode Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
c3e11bd223 |
chore(main): release 1.813.0 (#11141)
* chore(main): release 1.813.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> |
||
|
|
54553b2add |
fix(cli): resolve lockgen imports through modules a push leaves alone (#11160)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e8078f2a96 |
fix: dispatch workflow-as-code tasks from a deployed flow's inline step (#11146)
* fix: dispatch workflow-as-code tasks from a deployed flow's inline step * fix: give a workflow-as-code task its own result-cache key * fix: key a cached workflow-as-code task on its name and arguments * fix: hash a cached workflow-as-code task's arguments like any job's * chore: regenerate system prompts for the task cache_ttl docs * fix: key a cached workflow-as-code task on its step key, not its name * fix: key a cached workflow-as-code task on a fingerprint of its code * fix: keep the task() doc attached to task() * fix: key a cached inline task on its step key and the workflow input * docs: cache_ttl has no effect on a taskFlow target |
||
|
|
d0d5b295b9 |
docs: terminate datatable write examples with .execute() (#11148)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
129c045595 |
fix(cli): keep the workspace color when settings are synced from git (#11144)
* fix(cli): keep the workspace color when settings are synced from git Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(cli): name the sync direction consistently in the identity-field comments Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(cli): apply the workspace color from settings.yaml only when the file sets one Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
9fe493311d |
chore(main): release 1.812.0 (#11111)
* chore(main): release 1.812.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> |
||
|
|
a78beff743 |
feat: dynamic AI agent toolsets (#11050)
* feat: dynamic ai agent toolsets, and memory as a step input Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: address review round 1 on dynamic ai agent toolsets Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: tag enabled_tools and drop the memory step input Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: let an mcp server entry be named by the path the roster shows Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep $res: out of the tool names the enabled tools picker offers Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: name an mcp server by its bare path on the one side that can hold it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: count the enabled tool names that matched nothing instead of logging them Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: narrow an agent's roster in one pass, by whole entries Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test: pin that an mcp summary is rejected against a name that is not Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: regenerate the copilot flow schema after the merge Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: shorten the enabled tools list hint Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: take enabled_tools back to a plain list of tool names Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: keep the enabled tools add-menu hint describing the unset field Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: name a websearch tool that carries no summary of its own Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: reserve the name web search is enabled by so no tool can share it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: spell the reserved web search name with a hyphen Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: reserve __wm_web_search as the name web search is enabled by Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: advance ee-repo-ref past the git sync ci check work Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: shorten the enabled tools description the run form shows Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to e4c1b794d6c5e6e390987341b2840587bbb40348 This commit updates the EE repository reference after PR #785 was merged in windmill-ee-private. Previous ee-repo-ref: af668462f0f06b02a5f4e0c22e6156858487a518 New ee-repo-ref: e4c1b794d6c5e6e390987341b2840587bbb40348 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Ruben Fiszel <ruben@windmill.dev> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> |
||
|
|
42f489685b |
feat: store resource type display names and label hub integrations (#11113)
* feat: label resource types and integrations with hub display names Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: load hub integration names in the app and flow pickers Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: load hub resource type names where drawers title a type Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: store resource type display names and drop the hardcoded list Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: leave display_name out of the fork comparison Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: ignore over-long synced display names, move name loaders Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: share the hub integration list cache, backfill admins only Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep a name over a nameless duplicate, retry failed hub reads Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
75ee497011 |
fix(cli): stage a rewritten shared lockfile on git-sync deploy push (#11126)
* fix(cli): stage a rewritten shared lockfile on git-sync deploy push Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGciSSE5EFMiDf1dFWQq5M * test(cli): pin that a swept shared lockfile is committed as a deletion Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGciSSE5EFMiDf1dFWQq5M * chore: bump the git sync hub script to 28969 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGciSSE5EFMiDf1dFWQq5M --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
e8c02c04cd |
feat: windmill-chat sdk for chat-mode flows in external frontends and raw apps (#11117)
* feat: windmill-chat sdk for chat-mode flows in external frontends and raw apps Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018aQiZNAU8g17kWkyTryS5J * fix: keep streamed answers until persisted, finish turns after history fallback Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018aQiZNAU8g17kWkyTryS5J * feat: ai sdk transport and assistant-ui runtime for windmill-chat Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: finish a turn from the flow result until its answer row lands, hash chat ids without crypto.subtle Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: judge a turn answered by a persisted assistant row, wherever it was fetched Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: attribute a turn's answer to its own jobs, keep a local turn when switching conversations Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: mirror local history on every change, attribute failure-handler answers to the turn Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: new chat per token string in the React hook, idle after destroy, no reorder on view Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: recreate the hook's chat on any credential change, namespace local history per user Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: send the latest inputs from the React hook Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
a95e950529 |
feat(cli): list, get and restore trashed items with wmill trash (#11125)
* feat(cli): list, get and restore trashed items from the CLI * docs(cli): tell agents a sync push deletion is restorable with wmill trash * refactor(cli): share the ApiError formatting and type trash flags as integers |
||
|
|
d54a66f15c |
fix(cli): say where a sync push deleted variable or resource went (#10851)
* fix(cli): keep variables and resources a sync push repo never tracked `wmill sync push` archives a script it no longer finds locally, but hard-deletes a variable or a resource: the credentials go for good. A remote-only one is equally a deletion being deployed and one the repository never had, provisioned on the instance or written by a script at runtime, and reading the second as a deletion is unrecoverable. Committed history tells them apart. A push whose changeset deletes a variable or resource now asks what this branch has ever tracked at `*.variable.*` / `*.resource.*`; anything it has never recorded is kept on the remote (prompted for on a TTY), and a real deletion, recorded before the commit that removed it, still applies. Where the history cannot be read (shallow clone, sparse checkout, no repository) there is no evidence either way, so the deletion stands as before with a warning naming the remedy — the git-sync "Pull from repo" job runs in a depth-1 clone and must keep deploying the deletions it always has. `--delete-untracked-secrets` / `deleteUntrackedSecrets` opts a mirror-semantics pipeline back into deleting them unattended. Fixes GIT-980 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cli): classify secret-bearing deletions the way the push itself does Three ways the suffix match missed: - A fileset child can be any file, `inner.resource.yaml` included, and its deletion re-pushes the parent rather than deleting anything. Classifying with the push's own `getTypeStrFromPath`, behind the same fileset exclusion the apply loop uses, keeps the two in step. - Deleting `f/x.resource.file.ini` deletes the resource `f/x` outright, so without that file in the pathspecs every file resource walked past the check. Its two files now count as the one resource they delete. - A `specificItems` item is committed as `y.<workspace>.variable.yaml` while `elementsToMap` collapses it to the base path the changeset carries, so a deletion the user did commit read as never tracked. The history is searched under both names. `gitRecordedPaths` also reads its history with `core.quotePath=false`: a path with a non-ASCII byte came back C-quoted and matched nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cli): judge held-back deletions per object, not per file `DELETE /variables/delete` takes the resource at the same path down with it, and `DELETE /resources/delete` does the same to the variables its value references, so a tracked deletion could destroy an untracked object the push had just reported it was keeping. A file resource had the same shape from the other end: two files for one resource, either survivor deleting it. The unit is the server-side object. One file left unaccounted for by history now holds the whole object back, so nothing in a group reported as kept is deleted. The residual is a resource whose value references a variable at another path, which stays possible and is called out in the PR. Also corrects what the messages claim. Deleting a variable or resource is not irrecoverable: both move to the workspace trash, which keeps them for three days (migrations/20260326000000_trashbin.up.sql, CE since v1.665.0). The asymmetry with a script is real but narrower, and the prompt defaults to No, so it should say what it actually costs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cli): warn when sync push deletes variables the repo never tracked `sync push` deletes a remote variable or resource that has no local file. An object the repository has never tracked was provisioned outside it, by hand or by a script at runtime, rather than deleted from it, and the change list said nothing to tell the two apart. The push still deploys every deletion — that is what the repo-is-the-mirror contract means, and a shallow clone (the git-sync "Pull from repo" job, a default actions/checkout) could not tell them apart anyway. What changes is that the preview names the ones this branch's history has no record of, before the prompt that confirms them, and points at the excludes that stop them recurring. Deleting is also not final, which the CLI was alone in not saying: both handlers move the item to the workspace trash first, restorable for three days (migrations/20260326000000_trashbin.up.sql, CE since v1.665.0). A push that deleted any now says so. This replaces the earlier hold-back design. Keeping objects back changed what a push deploys, needed a flag and a wmill.yaml key to opt out of, and could claim to keep an object that a linked deletion then cascaded onto. Reporting cannot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cli): name the paths the untracked-deletion warning is about A push can delete a tracked and a never-tracked resource together, where "1 resource" identified neither. The warning lists the paths instead of counting them, so the reader knows which one to exclude. Outside a git checkout it no longer opens "This branch's history", which contradicted the reason it went on to give, and it drops the pronouns that disagreed with a plural count. The history walk is skipped under --json-output, where both notices are silenced and its result had no reader. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cli): vouch for an object with any file in history, not just deleted ones The tracked set was built from the deletions being judged, so a companion file the push was not deleting could not vouch for its object: a file resource whose `.resource.yaml` stays while its content file goes was reported as never tracked, though the repository plainly owned it. It is built from the whole history now, which also turns the workspace-specific lookup around — history is normalized to base paths, the form the changeset already carries, instead of each candidate being searched for under two names. The warning also prints one line per server-side object rather than per file, so a file resource is the one deletion it is rather than two. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cli): name both kinds at a shared path, gate the history path conversion Three from review: A variable and a resource at one path are judged together, since deleting either takes both, but they are two objects to name — keying the printed lines by path alone dropped one of them. `fromWorkspaceSpecificPath` strips a `.<workspace>` segment wherever it finds one, so a history entry that merely looks workspace-suffixed was re-keyed onto a different object, whose history then vouched for it. Only a path `specificItems` claims is converted now. Not reachable from a server object (the backend rejects `.` in paths), but history holds whatever was committed. `secretBearingKey` lost its last caller when the tracked set moved to object paths; removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cli): identify a secret-bearing object by kind as well as path A variable and a resource can share a path and are still two backend objects: `DELETE /variables/delete` drops the same-path resource unconditionally, while `DELETE /resources/delete` drops only the variables its value references. Keying tracked history by path alone let a committed variable vouch for a resource the repository never had, which then went unmentioned. The cascade is a reason to report both, not to treat them as one. A file resource's two files keep one id. `git log HEAD` also fails on a repository with no commits, which was reported as "its history could not be read. Check that git runs correctly in this directory" — true of neither. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(cli): tighten the comments on the untracked-deletion warning Halves the prose without dropping a constraint: the trashbin retention is stated where the message says it rather than twice more in doc comments, and the two stacked comments at the print site had come to contradict each other, one still describing a same-path variable and resource as judged together. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cli): say where a deleted variable or resource went `sync push` deletes a remote variable or resource that has no local file, and said nothing more. Both handlers move the item to the workspace trash first, restorable for three days (migrations/20260326000000_trashbin.up.sql, CE since v1.665.0), and the CLI was the one surface never to mention it — the report behind this concluded the deletion was final and there was nothing to restore. A push that deleted any now ends with where they went and how long they have. Drops the untracked-deletion warning this branch carried: distinguishing a deletion the repo deployed from an object it never owned needs the branch's git history, and roughly 130 lines to read it and be right about the answer, for a claim the trash already softens. Two fixes it turned up in the data-table migration guard, which reads history the same way, are kept: a path with a non-ASCII byte came back C-quoted and matched nothing, and a repository with no commits was reported as one where git does not run. Fixes GIT-980 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cli): name the trashbin correctly and say who can restore The tab is labelled Trashbin, not Trash, and `restore_trash_item` requires admin, so a non-admin reading the old line would go looking for a control they do not have. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(cli): move the migration-guard git fixes to their own PR They fix `gitRecordedDatatableMigrationPaths`, which this PR no longer touches. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(cli): don't count a .lock deletion the push skips The apply loop `continue`s past a non-raw-app, non-dbt `.lock` deletion before reaching the delete switch, so nothing happens on the server. The classifier did not mirror that, and `f/x.resource.file.lock` reaches it as a resource through `isFileResource` — a resource type whose format_extension is literally `lock` would have the notice announce a deletion the push never performed. A raw-app or dbt `.lock`, the two that loop does not skip, classifies as its bundle's own kind well before the file-resource check, so a suffix test is enough. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(cli): state what the classification tests protect The header described the change rather than the invariant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d8d7332eb6 |
feat: add per-route CORS origin allowlist for HTTP triggers (#10833)
* feat: add per-route CORS origin allowlist for HTTP triggers Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: fail closed on cold router cache and invalid origin input Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: resolve CORS route from the decoded path like the request handler Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: add instance-wide default allowed origins for HTTP routes Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: let non-superadmins read the default allowed origins setting Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: badge the advanced section when a route's origins are restricted Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: state inherited origins on the control and use one hint row Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: trim the origins tooltip and relabel the toggle when a default exists Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: keep the origins format hint visible until an entry is wrong Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: state the at-least-one requirement in the origins hint Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: import the origins validator in the trigger-http tests Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: make an empty allowlist deny rather than fall back to the default Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: address review nits on origin validation and the CORS editor * fix: derive the origins error from the stored list and tighten host validation * fix: parse real IPv6 hosts and refuse a newly emptied allowlist * refactor: make origin validation advisory except for null and non-ascii * feat: let an empty allowlist be saved as deny every origin * docs: document the empty allowlist as deny every origin * fix: bound allowlists, reject commas, and decide cors after the handler * chore: revert unrelated rustfmt churn in windmill-common tests * chore: revert unrelated rustfmt churn in windmill-common * chore: drop the route types the cors restructure replaced * fix: take the stricter cors decision from before and after the handler * fix: strip runnable cors headers when the routers are unavailable * docs: document the allowlist bounds in the openapi schema * fix: let an unavailable cors read defer to one that resolved * refactor: carry the resolved cors policy from the handler to the middleware * docs: describe why an unavailable read fails closed on the paths that reach it * fix: validate the default origins on the declarative settings path * test: keep the webhook doc comment with the test it describes * fix: warn on impossible schemes and ports, and validate the instance setting * feat: treat an empty allowlist as unset at both levels * perf: decode the cors path only when the fallback needs it * docs: document the empty allowlist as unset in the api schema * docs: describe an empty allowlist as unset in the frontend comments * docs: say what a null allowlist resolves to, not what it meant before the default existed * docs: state what the validator refuses and why methods stay broad * feat: exempt static asset routes from the origin allowlist * fix: hide the origin control for every static target, not just websites * fix: exempt only static websites, not single-file static assets * fix: warn on an unclosed ipv6 host in the origins advisory * fix: require assets present, not just the static website flag --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7b89e37322 |
chore(main): release 1.811.1 (#11107)
* chore(main): release 1.811.1 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> |
||
|
|
62d4632fad |
chore(main): release 1.811.0 (#11098)
* chore(main): release 1.811.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> |
||
|
|
c90d1d95c2 |
refactor: make the app policy's principal the authority for its identity (#10440)
* 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> |
||
|
|
4a293cf77a |
chore(main): release 1.810.0 (#11079)
* chore(main): release 1.810.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> |
||
|
|
30ffdbecc1 |
fix: unpin only the specifiers in the bundle a bun modules run executes (#11083)
* fix: keep version pins from imported scripts in bun lockfiles Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WCq4nkUjiZBnPPMuGYCo6w * fix: strip version pins from the bundle a bun modules run executes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WCq4nkUjiZBnPPMuGYCo6w * fix: unpin only module specifiers, not matching text elsewhere in the script Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WCq4nkUjiZBnPPMuGYCo6w * docs: name the raw endpoint lock generation fetches imports through Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WCq4nkUjiZBnPPMuGYCo6w * fix: leave require calls alone and skip spans not on a quote pair when unpinning Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WCq4nkUjiZBnPPMuGYCo6w * test: create the bun bundle cache dir a dependency job saves into Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WCq4nkUjiZBnPPMuGYCo6w * test: drop the lock test #11082's module test already covers Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WCq4nkUjiZBnPPMuGYCo6w * fix: narrow the change to a fail-open strip of the modules-run bundle Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WCq4nkUjiZBnPPMuGYCo6w * fix: unpin only the specifiers in the modules-run bundle, and log a parse fallback Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WCq4nkUjiZBnPPMuGYCo6w --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7915540f66 |
chore(main): release 1.809.0 (#11045)
* chore(main): release 1.809.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> |
||
|
|
63cb46d7bb |
feat: add a minimal skin for the approval page and slack/teams (#11061)
* feat: add an approval skin to the approval page and slack/teams messages Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9 * chore: point ee-repo-ref at the teams approval skin commit Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9 * fix: resolve the approval skin from the step awaiting approval Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9 * fix: shorten the slack approval message to fit the button value limit Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9 * fix: rename skins to detailed/minimal and keep long slack messages Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9 * feat: title the minimal approval page from the step and flow summaries Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9 * feat: let wait_for_approval set the description approvers see Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9 * fix: keep a finished workflow's approval description, still gated Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9 * fix: keep a login-required approval locked after the run moves on Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9 * feat: hide the windmill version on the approval page Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK2Ye4PizykReZVuMEm3m9 * chore: update ee-repo-ref to e92abc9d1fba3ba898640df0cfeb0af8a50849b4 This commit updates the EE repository reference after PR #786 was merged in windmill-ee-private. Previous ee-repo-ref: bf1766ff49458f62d3f11746f1a06893ae3c2325 New ee-repo-ref: e92abc9d1fba3ba898640df0cfeb0af8a50849b4 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> |
||
|
|
0af7675588 |
fix: keep an app's deployed policy on wmill push (#11049)
* fix: keep an app's deployed policy on wmill push Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * fix: keep a first push's file-stated app policy Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * fix: deploy a file-stated viewer app as viewer, not publisher Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * fix: never put a repo-stated run identity on the wire Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * fix: send the deployed run identity only when the push may claim it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * fix: let a file-stated execution mode win in both directions Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * refactor: drop the app-file execution mode helper with no caller Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * feat: warn before a raw-app push takes over the run-as user Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * fix: keep an app's run-as user when a push only deletes one of its files Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * refactor: settle an app's ownership check before any content parsing Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * fix: share one rule for which raw-app files a push sends Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * fix: stop tracking raw-app files no push ever sends Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * test: bundle for real instead of stubbing the module for every suite Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * docs: record why a module mock cannot be undone by afterAll Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * fix: keep tracking a runnable whose file shares a bundle-excluded name Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c * test: pin both halves of the backend runnable rule Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X47gNFqf1SsA67zTm71f8c --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8820b9fc64 |
feat: report script metadata with no content file in wmill lint (#11053)
Claude-Session: https://claude.ai/code/session_01PENfHNJceAbrcUsgfFCjXN Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |