mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-06 00:02:13 +00:00
2d09d33997318e2c42bdbf207bd70bd9df6cfe2c
14
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2d09d33997 |
fix(datatables): the three transactions that still locked before the settings row
Offboarding reassigns a departing user's scripts, flows, apps and resources — rows a rename writes while holding the settings row — so it takes that row first, and offboarding from several workspaces walks them in `workspace_id` order. Removing a member locked the `usr` row before the removal reached the settings row, and a rename renamed the `password` row before its first workspace: both now come after. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n |
||
|
|
42271dcf7f |
Merge remote-tracking branch 'origin/main' into datatable-perms-3
# Conflicts: # backend/ee-repo-ref.txt |
||
|
|
0fc9ba39a7 |
fix(datatables): a tenant and a resource path follow the principal they name
A data table role names its tenants — `u/alice`, `g/devs`, `f/team` — and a data table names its database by resource path. Both are strings that outlive what they point at, and the flows that free those names left them behind: - Deleting a workspace user, a group or a folder left its tenant on every role it could run as. The name is free afterwards, so whoever takes it next inherits the role without an admin granting it. All three now drop it in the same transaction that removes the principal. - Offboarding reassigns a user's resources to a folder without the data table following, which both stops it resolving and frees the path it named — a new resource there points it at another database with its roles intact. The username rename had the same gap. Both move the config with the resource. The walk both need is one function each now, with the table test that pins which tenant forms it touches. Saving permissions also takes a lock per data table, held from the read that plans to the write that persists: two saves interleaved each planned against what the other was leaving, and the one that persisted last stored roles the other had already dropped. The ACL apply takes the same lock, since it reads the same config and catalog. |
||
|
|
94af8d0fb5 |
fix: let a principal without a login account own a draft (#10925)
* fix: let a principal without a login account own a draft Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Lu3hExEDPZu2dAEhZDVAi * fix: keep an accountless draft owner from colliding or reading as legacy Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Lu3hExEDPZu2dAEhZDVAi * fix: drop the unnameable draft owner everywhere and guard the no-op rename Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Lu3hExEDPZu2dAEhZDVAi * fix: drop the unused Acquire import in the draft rename test Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Lu3hExEDPZu2dAEhZDVAi * docs: drop the stale draft_users claim from the fork-clone rationale Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Lu3hExEDPZu2dAEhZDVAi * chore: update ee-repo-ref to f5b783d2f7608e1ff3a817caa8b719e06f8b8981 This commit updates the EE repository reference after PR #768 was merged in windmill-ee-private. Previous ee-repo-ref: f3dba016e9274ee9bbe46b4f070d3ed29843e5fd New ee-repo-ref: f5b783d2f7608e1ff3a817caa8b719e06f8b8981 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> |
||
|
|
9c557859c5 |
feat: AI agent evals: datasets, scored runs and comparison (#10633)
* feat: eval datasets and standalone runs for reusable AI agents Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: agent eval drawer with case editor, runs and capture entry points Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: document AI agent eval datasets and standalone runs Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: say how many eval cases the list is not showing Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: address review findings on eval datasets - keep an edited case's conversation and tool inputs: serde(flatten) silently drops Box<RawValue> fields, so the update payload is spelled out - remount the case editor per case so one case's turns cannot leak into another - require jobs:read / flow_conversations:read on the capture endpoints, which UserDB does not gate by token scope - take the dataset lock in create and update so a delete cannot be undone by a concurrent metadata write, and delete cases before metadata - load more cases beyond the first page, and stop capping the agent picker - record that the version stamp is taken at enqueue, not at resolution Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: address round-2 review findings on eval datasets - block operators from dataset and case writes - pass the editor's operating workspace through the drawer and the capture request, instead of assuming the navigation workspace - discard superseded case-list responses so switching datasets cannot land the previous dataset's cases - reject a dataset without a case_id (or vice versa) rather than running an inline case under a dangling association - run unsaved edits inline instead of silently running the stored case - surface the API error body on a failed run - fetch dataset metadata concurrently when listing - $bindable() without a default on the optional open prop - correct the permission and enqueue-time-version wording in the docs Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: run an untouched saved case by reference again The editor writes back keys the stored case omits, so comparing the raw objects reported every unedited case as edited: the run went inline and lost the dataset/case stamp its history depends on. Compare a normalized form, and pin it with a test. Also scope the history query to the drawer's workspace and drop superseded responses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: show a dataset's cases as a table, and fix round-4 review findings The case list showed one case at a time with no overview. It is now a table with the case, where it was captured from, and its last run — the last-run column is a single jobs query on the path stamp rather than a request per row. Review fixes in the same file: - keep the edit baseline on the selected case rather than looking it up in the loaded page, so a case beyond page 1 is not treated as unedited and run stale - release the loading state when a superseded case load returns early - reload every loaded page after a write instead of collapsing to page 1 - last remaining 'resolved to' wording in the version tooltip Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: run a dataset as an experiment, with scorers as runnables An experiment runs every case of a dataset against one subject and records the exact case set it executed, so a result set stays reproducible while the dataset keeps changing. Each case runs as its own small flow — the agent, then a step per scorer — so a case keeps the run stamp, history query and trajectory view a single run already has, and scorers need no orchestration of their own. Results are read back per step by node id rather than by walking a nested loop's status. A scorer is any runnable taking (input, output, expected): a script, a flow, or a reusable agent used as a judge. A judge is prompted with the case and the answer as one JSON message; a script or flow receives them as named arguments. Scores accept a bare number, a boolean or {score}. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: results table for an experiment, with scorer columns One row per case: status, the agent's answer, and a column per scorer, with the mean per scorer above the table and a link into each case's run for its trajectory. Averages skip cases a scorer produced no number for — counting a missing score as zero would read as a regression. The drawer's left pane becomes Cases / Results, and Results carries the scorer picker and Run dataset. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: compare an experiment against a baseline Per-scorer deltas on each row and on the mean, and a filter down to the rows that regressed. Rows join by case id, so a case added after the baseline ran has no delta instead of counting as a change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: address round-5 review findings on experiments - match scorers by label when diffing two experiments; joining by array position subtracted one scorer from another whenever the scorer sets differed - report a row's status from the case job, not the agent step, so a case whose scorer failed no longer reads as a success - delete a dataset's experiments with it: they hold copies of its cases, and a recreated dataset of the same path would have exposed them - select the experiment that Run dataset just started instead of leaving the table on the previous one - expected is scored now, so stop describing it as having no consumer Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: address round-6 review findings on experiments - hold the dataset lock across an experiment launch, so a delete landing between reading the cases and writing the experiment cannot recreate the deleted dataset's inputs - match scorers between experiments on kind and path, not on label: labels default to a path's last segment, so f/a/quality and f/b/quality compared against each other - average mean deltas over the cases both runs scored; comparing each run's own average reported a regression from a case the baseline never ran, with no regressed row to point at - openapi: the row status is the job's, which is also canceled/skipped; runEval takes scorers; the update-case body no longer advertises source, which the handler deliberately ignores - record why the experiment prefix cannot reach a sibling dataset Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: address round-7 review findings on experiments - release the dataset lock for the push loop and retake it for the write, re-checking the dataset still exists: holding it across the whole launch made every capture and case edit on that dataset 409 until the last job queued - assemble experiment results with bounded concurrency; a 100-case, 3-scorer experiment was 400 sequential lookups, each itself several queries - clear the baseline when it becomes the selected experiment, which was comparing a run against itself and reporting zero deltas - take the header mean over the same cases as its delta while comparing, so the two numbers beside each other describe the same set - a canceled or skipped case is no longer the same grey dot as a running one Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: address round-8 review findings on experiments - verify the dataset's identity, not just its existence, before recording an experiment: the path can be deleted and recreated during the push loop, and the experiment holds copies of the old dataset's cases - give the recording lock a longer budget than a case edit, since its jobs are already queued and giving up strands them, and say so when it fails - keep score lookups sequential within a case: nesting two bounded streams multiplied into 32 in-flight queries against a 50-connection pool - clear a baseline that no longer belongs to the loaded experiments, so switching datasets does not leave comparison mode on with nothing to compare - keep a scorer's own mean when the baseline never ran it, instead of blanking a column full of numbers - EvalCaseDraft.expected no longer claims nothing scores it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: do not trust an experiment's job ids, and require write to record one Experiment objects live in workspace object storage, which a script can write directly, and results are read on the unrestricted pool — so a forged experiment naming another flow job returned output the jobs API would have refused. Only jobs this server stamped with that experiment's id are read now. Also from round 9: - recording an experiment requires write on the dataset, not read: it persists into the dataset's namespace and its shared list - clear the results table when the selection changes and surface a failed load, instead of labelling the previous experiment's numbers as the new one's - a storage fault is no longer reported as a deleted dataset - the lock-timeout message at the recording site no longer says to retry, which would run the whole dataset again on top of the jobs already queued - ExperimentRow.status documents canceled and skipped Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: bind the experiment trust check to the requested dataset The previous check matched jobs on the experiment id alone, which the stored object supplies — so copying another dataset's experiment JSON under a readable key carried its jobs' output along with it. A job is now only read if it was stamped for this experiment *and* for the dataset the caller's read access was checked against, and an experiment that names a different dataset is not served from this key at all. Also from round 10: - add the .sqlx entry for that query; without it every SQLX_OFFLINE build failed - serve results over GET: as POST the route-scope middleware classified a read as ai_evals:write, locking read-only tokens out of their own results - clear the selected and baseline experiments synchronously when the dataset changes, so the previous dataset's id is not requested under the new one Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: address round-11 review findings on experiments and scorers - give scorers the whole case input, not just the message: an answer that came from attachments or a replayed conversation could not be judged on it - accept a judge's boolean and structured {score} answers, including stringified ones, and pin every documented scorer shape with a test - record an experiment for the cases that did launch when a later push fails, instead of leaving those jobs running with nothing to attribute them to - do not capture a preview parent's synthetic runnable_path as a host flow; the saved case could not be rerun - clear the case table before loading a dataset and surface a failed load, so a failure cannot leave the previous dataset's cases under the new name - keep the results table through a refresh of the same experiment - exclude flow-step jobs from the per-case last-run lookup - drop case sets from the experiment list, which is only used to pick a run - report a database failure at the recording lock as itself, not as contention Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: address round-12 review findings on capture and run history - load flow_node.flow for flownode parents: an agent inside a deployed branch or loop captured without its agent, host flow or tool bindings - decide host_flow_path by whether the path resolves to a flow, not by job kind: excluding previews wholesale also dropped the flow editor's step test, whose path is real - page the per-case last-run lookup by created_before until the loaded cases are covered; one page of 200 reported older cases as never run - do not record an experiment when nothing launched - only attach the case input to a job when a scorer will read it - keep the case table through a save; only a different dataset clears it - drop the superseded duplicate comment on the score parser Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: stop refetching run history on every case write Reading the case list before the first await made the whole job-history query a dependency of it, so every save, delete and Load more refetched up to 1000 job rows and blanked the column. Read untracked instead. - an empty Last run cell now distinguishes never-ran from not-found-within the page bound, which the comment already claimed and the cell did not - reloading a dataset no longer replaces a populated table with a skeleton - keep the score-parser comment that describes every shape it handles Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: keep eval datasets in Postgres instead of object storage Datasets, cases and experiments become rows (`eval_dataset`, `eval_case`, `eval_experiment`, `eval_experiment_case`) rather than objects under a `wmill_eval_datasets/` prefix. What a run produced is still the job's: only case inputs and an experiment's case snapshot are stored. This removes the machinery the object store needed: - The advisory lock and the read-modify-write of a per-dataset JSONL. A case is a row, so there is nothing to serialize. - The launch-time identity check on the dataset. The foreign key makes a concurrent delete fail the transaction instead. - The trust guard on an experiment's job ids, which existed because a script can write workspace object storage directly and could forge an experiment naming somebody else's job. An experiment now chooses every job id and records itself before pushing anything, so a launch that dies partway leaves a recorded case whose job is missing rather than a running job nothing accounts for; cases that never reached the queue are removed again. Row-level security on `eval_dataset` is the authority on who may read or write a dataset, so `extra_perms` grants work and the rule is not mirrored in Rust. Cases and experiments carry a read policy derived from their dataset and no write policy: they are written on the unrestricted pool after the dataset row itself has been asked, with `SELECT ... FOR UPDATE`, whether the caller may write it. Cases are capped at 256 KiB each and 10 000 per dataset, refused rather than truncated. Attachments are S3 references, not inline bytes, so a case that approaches either cap is a mistake rather than a use case. Evals no longer need the `parquet` feature or a configured workspace object storage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * style: align the eval drawer with the design system - Scorer chips are `Badge`s rather than a hand-rolled bordered span, and the section header is a `Label` with its tooltip, as are the case editor's fields (which also gets the label colour right). - The results table showed status as a coloured bullet, which says nothing to a colour-blind reader. It now carries the same icons the runs table uses, with the status as its accessible name. - Feedback colours move to the `-500` shades the brand guidelines name. - The conversation JSON error uses `TextInput`'s `error` prop for the border and the caption style for the message, as elsewhere. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: author an expected answer, tags and attachments on a case Every scorer is handed `(input, output, expected)`, but nothing could produce an `expected` except a conversation capture: the case editor had no field for it and a captured run left it empty. So: - The editor gains Expected, Tags and a read-only list of the attachments a captured case carries. Expected is plain text, or JSON when the answer has structure. - Capturing from an AI agent run keeps what that run answered, which is the only moment a reference answer exists for free. The results table also laid itself out by content, so a long answer pushed the scores — the numbers the table exists for — off the edge of the pane. It is fixed-layout now, with the text columns bounded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: expected is captured from a run and can be authored Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: link a saved agent when inserting an ai agent step "AI Agent" in the step picker was a leaf that always created a blank step, so reusing a saved agent meant inserting a blank one, opening its step input and linking it there. It is a category now, like Flow and AI Sandbox, listing the workspace's `ai_agent` resources next to a blank option, filtered by the picker's own search. A picked agent produces a step that is already linked rather than one linked afterwards: `agent` set, no tools, and only the flow-local `user_message`/`user_attachments` transforms. Seeding the brain keys there would leave transforms a linked step never reads and that `AgentResourceBar` strips on its next link change. Each `on:new` forwarder rebuilds the insert detail field by field instead of spreading it, so a new field is dropped unless the forwarder names it. `agentPath` is typed on both `GraphEventHandlers.insert` and `FlowGraphV2`'s `onInsert` so the next one to forget it fails the check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: restore the link on cancel and simplify the agent bar Cancel on an agent edit forked the step into a standalone copy, which is the opposite of what the word means and needed a paragraph under the card to explain. It discards the edits and re-links the step now, leaving the agent untouched; diverging from an agent is Unlink's job, on the linked card. This flow's `tool_inputs` survive the round trip as overrides, so Cancel no longer folds them into the tools the way Unlink does. Linking a step to a saved agent happens in the step picker at insert time, so the bar's own resource picker is gone and "Save as agent" is the one action left. Its `+` button was a trap besides: it opened the generic resource form, where an agent would have to be written as raw JSON. The card itself was `surface-secondary`, the sections token, so in dark mode it was darker than the pane and read as a sunken well rather than an elevated card. It uses `surface-tertiary` as the brand table prescribes, its tool chips are `Badge`s, and the editing card no longer overflows the pane and clips its own buttons. The remaining tooltip follows the inline `Label` convention rather than sitting in a flex row whose gap stacked on the trigger's own margin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: rework the AI agent evals surface into one table Evals become a single pane: a dataset of cases, one column per scorer, one row per case, with the run being looked at chosen from the toolbar. Runs are permanent. Running the whole dataset opens one; running a single case records nothing at all — it is a job, and looking at what it did is not a claim that it belongs in the history. Its result and its scores sit over the row until they are saved as a run, which carries the cases that were not rerun and the scoring jobs themselves, so the number that is saved is the number that was looked at. A scorer is a runnable: a judge agent or a script, created in one click and edited in place. Scores carry a reason and per-assertion checks, shown on hover with a rescore button. What ran is always named. A run records the agent version, or — for a configuration that is not deployed — a hash of it, so a table can say that its numbers describe an agent that no longer exists: those rows dim and the table offers to rerun. An agent's draft can be run directly instead of the deployed value, and once those edits are deployed the runs that made them are recognised as that version. A step with no agent of its own is evaluable too, and saving it as an agent moves its history onto it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: keep an agent's in-progress edits on the agent Editing a linked agent forks it into the step, which is what makes the edits runnable there — but the agent is what is being edited, so that is where the unsaved state belongs. The edit is mirrored into the agent's own resource draft as it is made. It then survives leaving the flow, shows the agent as drafted wherever it appears, and is what evals run when asked to run the draft rather than what is deployed. Deploying or cancelling clears it; opening Edit without changing anything does not mark the agent as drafted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: shape the evals surface around a saved agent Evals hang off an `ai_agent` resource, so the surface is now only ever about one: the `draft` subject kind, the standalone-step subject and the move that carried a step's history onto a newly saved agent are gone. - A run is permanent and numbered per agent. Running a single case is a trial: it answers in the panel and never touches the table. - "Run scorers only" opens a run of its own that reuses the answers of the run you are looking at, so a scorer added later measures what already ran without calling the agent again. - A draft run whose configuration is later deployed is stamped, once, to the version it became, so its label stops reading `v23 + edits` forever. - A scorer can carry a pass threshold, read off the scores already recorded. - The table is the case, its answer and one number per scorer; datasets are created and edited in a drawer; a run that executed an earlier state of the current draft says so above the table, in one line. - Which agent a step is, whether it is being edited, and which version it is on is a strip above the step's tabs, because it is true of every tab. - Capturing a case from a step test or a conversation is dropped, and with it the `memory` override on a linked step that nothing set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: run past versions of an agent, and number versions per resource The evals home becomes one table of every run of the agent, whichever dataset each is of, with one badge per scorer. A list spanning datasets cannot hold every dataset's scorers to look a name up, so a score carries its name and kind with its number, and thresholds are joined in per run and column. Run now asks what to run: the latest agent, resolved when the run executes as a flow step does, any past version, or the unsaved edits. Pinning is a subject kind of its own, since a linked step resolves the resource live and inlining is the only way to run a version that is no longer current. Scorers move into the edit-dataset drawer. The column header over a run reports and nothing else: a run is permanent, and a control there that changed the columns would edit the past from the one place that must not. Adding one offers four ways rather than two, writing and reusing being different jobs, and both new kinds open with a summary filled in. Versions are numbered per resource. `resource_version.id` is one identity sequence for the whole table, so an agent saved nine times read v4 ... v24, and the gaps counted writes in workspaces the reader cannot see. The id stays how a version is addressed; the new number is what it is called, in the resource history drawer as well as here. It is assigned on write rather than counted on read because trimming past the cap and clearing a history both take the oldest rows, and counting the survivors would renumber a version a run already names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: read the dataset a remembered selection names Reopening the evals modal restored the last dataset from storage as a bare path, without reading the row it names. Every "is this already the one?" test compared against that selection, so all of them short-circuited and the dataset was never loaded: editing it opened a drawer with no summary, no scorers and no cases. The remembered path is now brought into context the same way any other choice is, and the tests compare against the dataset that is loaded rather than the one that is selected, so a selection can no longer stand for a read that did not happen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: give dialogs a trail in their header A dialog deep enough to navigate had nowhere to say where you were: the header held a fixed title, and the way back was a control each body placed for itself, somewhere in a toolbar that moves with everything else the toolbar holds. The header is the one part of the surface that does not move, which is where the trail belongs. `Modal` takes an optional `trail` of levels below its title, rendered as a breadcrumb whose ancestors are the way back. Declarative on purpose: callers of this depth already hold the state that says where they are, so the dialog reads it rather than owning a stack they would have to push and pop in step with it. Escape follows the trail. Leaving a level is what someone deep in a dialog means by it, and closing the whole surface throws away the navigating they did to get there; at the root it closes as before. That only works if a dialog can tell it is the surface being addressed, so `Disposable` now answers `isTopmost()` and the dialog asks before acting: it keeps Escape for itself, so nothing else was arbitrating between it and a drawer opened from inside it, and both were acting on one key press. Evals is the first caller: its runs list is the root, a run is a level in it, and the back button that used to sit above the table is gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: portal dialogs out of wherever they were opened from A dialog rendered in place inherits whatever the calling component happens to sit inside. One `transform`, `filter` or `overflow` anywhere above it makes its `fixed` positioning resolve against that ancestor instead of the viewport, and a surface meant to cover the app is then confined to a box it never asked for: the nav rail paints over it and its own edges are clipped. Drawers have always portalled for this reason. Dialogs only did so when an enclosing pane claimed them, and rendered in place otherwise, so the same screen could show a drawer over everything and a dialog trapped behind the nav. They now portal the same way: to the pane when one claims it, to `body` otherwise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: make the dialog's title the first step of its trail The trail listed levels below the title, so a dialog one level deep read "Evals > All runs > Run 20 · v6": three steps for two places, the first two of them the same place under different names. The title is the root, so it is the root's own segment, and the trail a dialog is given is now the whole path with that segment at its head. Its height stopped moving too. A heading carries a line-height of its own, so a header holding only an h3 stood six pixels shorter than one holding segments as well, and the dialog's whole top edge stepped as you navigated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: sharpen the evals controls around where you are standing Each screen now offers what belongs to it. The list starts runs; a run is a record, so it offers only the one thing that acts on the record itself, which is measuring the answers it already stored. Starting a fresh run from inside one asked which agent and which dataset from the screen least about either, and scoring an existing run was offered from the list, where there is no run to score. Which run and what it is read against are one question asked twice, so they sit together rather than at opposite ends of a row. Choosing what to run is now a toggle over the two states worth naming, the draft and the saved agent, with every earlier version one click further: running an old version is deliberate, and a list made all three look alike. The draft is read when the dialog opens rather than taken from the caller's polled copy, which could be seconds behind an agent edited a moment ago and would leave the option out exactly when it is the reason for opening the dialog. The dataset field carries its path under it and its edit button on hover, as a resource picker does, so the closed field says what the open list said. Edits waiting on an agent are a "draft" here as everywhere else in Windmill, rather than "+ edits". The dialog runs an evaluation rather than "the agent", which is what it was already called everywhere it is recorded. An agent being edited keeps its evals button on a line of its own, clear of the decision to save or discard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: settle the evals controls on the patterns Windmill already has The version choice uses ToggleButtonMore, as the AI provider picker does: the two states worth naming stay in the group, the rest are behind the overflow menu, and the one you pick joins the group rather than appearing in a second control below it. The deployed one says which version it resolves to. A run offers nothing to start. Scoring an existing run again was the last thing left there, and it was one button explaining a distinction that the run and the dataset already make between them. The warning that a run executed an earlier draft is about the run on screen, so it goes when the run does rather than following you back to the list, and it sits against the table instead of inside a frame of its own. A dataset just created stays open for its scorers and cases: those are what a dataset is, they can only be added to one that exists, and closing on create sent you to find it again to add them. Scorer settings are a cog rather than a word, now that the row holds three actions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: close the gap in the version toggle and say what naming a dataset does The overflow trigger is not a pill, so the room it reserves showed as a gap between it and the button before it; it is pulled in by that much. The dataset field gets its clear button, which is also the slot the edit button is positioned against, so the two now sit where a resource picker puts them. Naming a new dataset said nothing about what happens next, and the drawer looked like it was missing the rest of itself. It says so instead: a scorer and a case both belong to a dataset, so there is nothing to attach either to until this one exists, and creating it leaves the drawer open on them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: choose a dataset's scorers while naming it A scorer is a reference to a runnable, not a child of the dataset, so it needs the dataset's name but not its row. The list is collected in the drawer while the dataset is being named and sent with the create, which already accepts one, so a dataset arrives holding the columns that were chosen for it rather than being made empty and then edited to hold them. Cases stay where they were: a case *is* a row of the dataset, so there is nothing for it to be a row of until one exists. The drawer says which of the two is which instead of leaving the screen looking like it is missing the rest of itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: level the version toggle and name the dataset in its own field The overflow trigger stands a row taller than a toggle button, so the group grew to its height and left the sunken background showing under every pill beside it. Every child of the group is the same height now, which is why the AI provider picker never had the band: it sizes them all alike. The dataset field says the summary with the path after it rather than carrying the path on a line below. The list stacks the two, which a one-line field cannot do, so it says both the other way round. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: tidy the evals forms and the run's own controls Picking a scorer that exists chooses between two sources rather than showing both: the ones already measuring something, and everything else in the workspace. The first list says what each is called with its path under it and what it already measures on the right, instead of three columns that were the same path truncated three ways whenever a scorer had no name of its own. A dataset's drawer says what it is for on the page rather than under an icon, and its summary is sized like the field beneath it. The run's own row lines up with the table under it, the warning above that table is spaced off the rule rather than sitting on it, and adding a case is gone from a run: a run is a record of cases that were answered, so curating them from it is editing what it measured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: create a dataset holding the cases written for it Creating a dataset takes the cases to create it with, so one can be assembled in a single act instead of made empty and then filled in. The drawer holds them while the dataset is being named, gives them ids of its own to be edited by, and sends them with the create. Every case is checked before the dataset is written. `eval_case` grants users no write, so the rows cannot be inserted in the transaction that creates the dataset under the caller's own policies; validating first is what keeps "created holding these cases" from becoming "created, holding some of them", and the rows that do follow go in one transaction of their own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: name the button for what it opens, and say what each version is Starting an evaluation asks which state of the agent and which dataset, and both cost a provider bill, so a button that read as spending one on the way past was lying about the click. It opens something, and says so. Running one case from the panel keeps its own name and its play icon, because that one does run on click. The version options say what they are rather than what they are not: what a flow step would or would not run is a fact about somewhere else, and someone choosing what to evaluate is not standing in it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: give the editing card two rows and mark evals as beta At the width of a step panel the card's one row wrapped: the line naming the agent, the line saying what saving does, and the two buttons deciding the edits' fate all fought for it. Deciding gets a row of its own, and evals sits against the line it is about, since evals of an agent being edited run the edits. Evals is named wherever it is offered. It read as a word in one state of the card and as an icon in the other, which is two things to recognise for one door. The dialog carries a beta badge against its own name, before any level below it: every way in lands there, so it is said once and stays put as you navigate. The version toggle spells out which is which. Both are the agent at v2 and the difference between them is the whole choice, so it is worth the width. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: name a new dataset, and lay the scorer's settings out like a step's inputs A new dataset arrives called "Dataset 1", which the path follows as it follows any summary: a dataset with none was one every table could only call by its path, and the two seeds are what the summary rule already produces. Scorer settings put each field's description between its label and its input, where a step's inputs put theirs, and its inputs are the size the rest of the drawer uses. The runnable behind the column is a link to it with its kind's icon, since it is a resource of its own and the one thing about it these fields cannot change. The line explaining that a pass line re-reads recorded scores went: the threshold is a number to set, and how it is applied is not a decision being made here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: curate a dataset in the drawer and save it in one act The drawer holds the cases while they are edited and writes them when it is saved: added, changed and dropped, whichever it is. Typing no longer writes, so a set is never half saved while someone is still deciding what is in it, and Save means the same thing whether the dataset exists yet or not. A case panel offers reading rather than acting. Running one case now and editing one from a run were the last two ways to change a record from the screen showing it, and the machinery behind the first went with it. The answer is rendered as the prose it is, under what it is: the case's result, whichever run is selected above it. The rest is what the run's table was doing to its own edges: a column name is clipped to its column rather than running into the next, the table squares off against an open panel, and that panel closes with the run it belonged to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: one border above a table, and a link to the run's job The row above the table drew a bottom border and the table draws its own top edge, so every table sat under two lines. The row keeps its spacing and the table keeps its edge. A column header no longer spins while its scores arrive: the cells under it are where the numbers are missing, and they say so themselves. The beta badge is the height of the word beside it rather than of the line it sits on. A run is one flow and therefore one job, so the run says where that job is: what it is doing, what it cost and what it logged are all there rather than reconstructed from the table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: stream scores as each scorer finishes, and show them per case A scorer runs after the agent inside the case's own iteration, so its verdict can be read as soon as its step is done. Waiting for the iteration to end held every column of a case back until the last of them finished, which is why answers arrived one at a time and scores all at once. Reading a job that is still running needs one guard: a module with nothing in it is a step that has not run, not one that produced nothing, and recording the second makes a failure that never goes away. The panel beside the table shows what each column made of the case and why. The reason a judge gave was stored and never shown, which is the half of a score that says anything. It stops repeating the question the header already asks, and a case still running reads as waiting rather than as an answer that says "Running". A run is a number beside a dataset, so the list puts the two together. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: score a case with every scorer at once The scorers of a case read the answer and never each other, so they ran one after another for no reason: measuring a case now takes as long as its slowest column rather than as long as all of them. Each is a branch of its own, kept from failing the others, so a judge that errors costs its own column and no more. An iteration is three steps again — answer, payload, scores — rather than one per scorer, and each branch is named for the column it produces, so the graph of a run says which scorer did what instead of spelling out an id. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: read a judge's score out of the JSON it nearly wrote A judge quoting the agent inside its own reason writes those quotes unescaped, which is invalid JSON and also the most ordinary sentence for it to produce. The whole verdict was being thrown away over it, so a column that had a number reported having none. The number and the reason are now read straight out of such text. Deliberately not a second JSON parser: it finds the two keys and takes what follows, which is what survives a quote in the middle of a sentence. A case still running says so with a spinner rather than with the word "Running" sitting where its answer goes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: ask a judge for a shape instead of trusting it to write one A new judge carries an output schema, so the provider holds it to `{score, reason}` rather than the prompt asking it to. Windmill already delivers a schema whichever way the model takes it, a tool for Claude and Bedrock and the native parameter elsewhere, so there is no list of models to keep here. An agent with no runs offers its first one where the first row would be, rather than from a toolbar above a table that has nothing in it. Starting a run no longer picks a dataset for you. It fell back to whichever came first, which on an agent that has never run means offering another agent's set as though it were the obvious one; and with no dataset at all it says so and offers the one move there is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: report a column that failed throughout, and hold the run dialog The runs overview dropped any column that produced no number, so a judge that failed on every case of a run vanished from the row and read as a column nobody had asked for. The aggregate now reports every column that has cells, with the count of the ones it failed on, and the badge says "failed" where there is nothing to average. A column with no cells at all is still left out: that one was added after the run and has nothing to say about it. Creating a dataset closes the drawer rather than turning it into an edit of what it just made: scorers and cases already ship with the create, so there is nothing left to stay open for. Reached from the run dialog, it gives the screen back with the new dataset selected, and the dialog keeps the version you had already chosen. Also: - the case panel's job link moves to the panel's own header, where its scope is: the job is the whole iteration, not the answer it sat over - one action in the scorer drawer's header, as its neighbours have. The reuse list picks rather than adds, and says which dataset each column already measures - adding a case is the last row of the list it lands in - the pane shows what it has read rather than an empty state it has not earned yet, and its rows say they open - the linked agent card loses a border it had inside another one * fix: keep the linked agent card's outline The card is a thing inside the step's inputs rather than a section of them, and the outline is what says so. Only the rule inside it goes: the detail it separates is already set apart by being detail. * refactor: fit the eval surface to the shipped design * feat: give a nested dialog a back control and the runs list its own moves Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: put a dialog's description under its title Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: fold a dialog's back control into the crumb it returns to Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: edit a dataset's cases as a table rather than a list beside a form Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: edit a dataset's cases in the grid the data tables are edited in Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: edit a grid cell of prose in place, and cap a dataset at one page Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: keep the cell editor's styles beside it, not in the vendored theme Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: keep an empty cell empty and cap the editor's growth Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: name the step that assembles a run for the scorers Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: run the payload step natively, and say so when nothing serves that tag Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: report an answer as answered while its scorers are still running Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: let a scorer say a case is not one it measures Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: score the answer, and leave a case with no expected answer unmeasured Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: split the evals backend into modules Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: record what a run produced so it outlives its jobs * fix: read only the agent step's own tool jobs into the payload * fix: pin a run's configuration and give the judge the attachments * feat: write a dataset's cases in one transaction * chore: refresh the sqlx cache for the eval queries * fix: drop results a newer selection has superseded * fix: keep a draft the agent editor never opened on * feat: let a run record what it produced instead of waiting to be read * fix: serialize the replacements of a dataset's cases * fix: stop the poller from superseding a read slower than its interval * chore: refresh the sqlx cache * fix: keep a failed read from settling a cell as a case with no answer * fix: hold the case grid while its save is in flight * fix: keep a failed collect step from failing the run it recorded * chore: refresh the sqlx cache * fix: commit an open cell into the save that reads it * refactor: size the eval buttons with unifiedSize * docs: describe a run as the one flow it is * fix: show a run's recorded rows when part of it cannot be collected * refactor: size the remaining PR-added buttons with unifiedSize * fix: save the dataset name that was submitted, not the one typed after * fix: force an open cell into the save that was pressed for it * fix: refuse to score a run whose evidence could not be read * fix: hold one lock over a dataset's case count and its writes * fix: keep one unreadable run from costing the whole runs list * refactor: drop the banned bindable-default from the eval props * fix: hold the scorer controls while the dataset is written * fix: read only the caller's own draft of an agent * docs: say in the contract that a run pins its configuration * fix: say a scorer did not run rather than blaming a missing answer * feat: resume the agent draft you already had when you press Edit * refactor: build the trail and dataset controls from Button * fix: clear the open-cell flag when the drawer reopens * chore: refresh the sqlx cache * fix: read a run's configuration and its version from one snapshot * fix: refuse a dataset path or summary the column cannot hold * refactor: handle the agent draft the way the resource editor does * fix: run only a configuration the launch actually read * docs: bound dataset path and summary where they are submitted * fix: surface a stalled agent draft instead of claiming it is kept * fix: stop claiming a draft holds edits a failed write never sent * fix: word a missing score only once the run says whether the case answered * fix: let a breadcrumb crumb shrink so its truncation applies * docs: describe where an agent's unsaved edits live and what drops them * fix: keep harvesting scores when the run cannot yet word a missing one * fix: report a refused draft write the card was reading as a save * fix: drop the refused draft write when the server copy is taken instead * refactor: build the scorer and dataset pickers from the design system * fix: say what removing a scorer column actually does * fix: drop a refused draft write wherever the server copy is read * fix: let a picker row be as tall as the two lines it holds * docs: record what removing a scorer column does to recorded runs * fix: send a queued draft write before reopening, and drop only what it refuses * refactor: write the agent draft at commit points instead of mirroring keystrokes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: run an agent's edits from the step instead of keeping them as a draft Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: make the diff badge keyboard operable and refuse an edits run without its edits Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: drop the dataset icon from the scorer picker rows Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: size the evals buttons like the rest of windmill and call a run of edits edits Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: count a brain expression as an edit of the linked agent * fix: cap scorers per dataset and report a launched run as launched * fix: harvest scores in one read, refuse duplicate case ids, allow group paths * fix: mint scorer ids server-side, save a dataset edit in one request, check attachments * fix: write a dataset edit and its cases in one transaction * fix: atomic dataset create/edit, reset eval pane per agent, stable pending scorer ids * refactor: govern eval_case writes by RLS so a dataset edit is one transaction * fix: pin launch snapshot, order case locks, cap dataset size, guard stale load * fix: cap dataset bytes on single-case writes, reset run-dialog flag on load failure * feat: migrate eval datasets on username change, settle unspawned cases, drop unused case endpoints * fix: resolve scorer scripts as the caller and pin their hash; migrate scorer paths on rename * fix: bound a failed tool call's error to the payload truncation cap * fix: pin scorer hash as a hex string, reject missing judges, migrate eval authorship * fix: record an out-of-range scorer result as an error, not a score * fix: resolve judges in one caller-scoped read, pin deployed scripts, bound pass_if * fix: settle unspawned cases only when the run completes, and their score cells too * feat: reassign eval datasets and their path references when offboarding a user * fix: use the regex backreference in offboarding eval path rewrites * fix: register eval datasets in offboarding registries, keep resource-version param name * refactor: name the resource-version path param id, since it is the row id not the version * fix: validate dataset paths canonically, clone eval data on fork, surface eval load and launch failures * docs: note MCP tool results are not yet surfaced to eval scorers * fix: show the eval error state on any load failure, not only an empty dataset list * fix: preserve eval case order across a batched save Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjewUvXFEjNLLw7kxz41h * docs: scope the eval launch delete-safety guarantee to the assembly window Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjewUvXFEjNLLw7kxz41h * fix: only offer deployed scripts as eval scorers, drop unbuilt rescore claim Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjewUvXFEjNLLw7kxz41h * fix: enforce 0-1 scorer threshold in the settings drawer and clear stale eval load errors Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjewUvXFEjNLLw7kxz41h * fix: scope subject version/hash reads to the caller and keep a 0 pass threshold Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjewUvXFEjNLLw7kxz41h * fix: select the saved dataset when creating or renaming from the Run dialog Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjewUvXFEjNLLw7kxz41h * fix: gate eval dataset rename on path ownership, not just write access Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjewUvXFEjNLLw7kxz41h * fix: tolerate a malformed agent config when resolving the deployed label Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjewUvXFEjNLLw7kxz41h * refactor: trim eval code and comments, fix shared select and modal paths Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: drop the rename warning when editing an eval dataset path Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: add eval dataset delete, keep summary on partial edits, settle resultless scorer cells Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: cover parseThreshold and subjectLabel Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: hold dataset Save during a scorer write, derive draft_hash only from the carried draft Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c2deea13b7 |
fix(security): a WM_TOKEN job token can never be a global superadmin (GHSA-hfh4-cx4h-3fcr) (#10124)
* fix(security): a WM_TOKEN job token can never be a global superadmin (GHSA-hfh4-cx4h-3fcr)
Privilege escalation: an app/flow/schedule/trigger execution policy's `on_behalf_of`
(which a `wm_deployers` member can set) could point at a superadmin email. The
resulting job `WM_TOKEN` then passed the email-based superadmin checks, granting
instance superadmin. `forbid_superadmin_job_token` only guarded ~15 of ~75 routes.
Fix at the token layer: a WM_TOKEN must never satisfy a superadmin gate,
regardless of whose email it runs as (sentinel OR a real superadmin).
- `ApiAuthed` gains a `job_id` field, stamped once in `AuthCache::get_opt_job_authed`
from the resolved token's job_id (correct even on cache hits).
- `require_super_admin(db, email)` -> `require_super_admin(db, &ApiAuthed)`, rejects
`authed.job_id.is_some()`. `require_super_admin_email` kept for the few internal
callers without an ApiAuthed.
- `is_super_admin_authed(db, &ApiAuthed)` for the boolean `is_super_admin_email`
authorization branches on request handlers (workspace deletion, fork drops,
dev-workspace attach/archive, object-storage SSRF exemption, custom dbname, EE GHES
+ connected repositories, ...). Migrate ~75 sites (OSS + EE).
- CUSTOM_INSTANCE_DB reads the *authenticated* job_id, not the caller-supplied
`?job_id` query param. Worker-tag check takes a precomputed job-aware `is_super_admin`
on the request path.
Execution-time on-behalf checks (scheduled/flow worker-tag, Cloud enqueue quota,
is_devops_email) are hardened in a follow-up — see
docs/followup-onbehalf-execution-privilege-hardening.md.
Regression tests: a superadmin-email WM_TOKEN is rejected on `require_super_admin`
routes, on `DELETE /workspaces/delete/{w}` (403, workspace preserved), and on the
CUSTOM_INSTANCE_DB lookup with no `?job_id` (401); real superadmin tokens still succeed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: cap devops role at workspace admin and reject reserved on_behalf_of identities
Extends the job-token cap with three pieces:
- `require_devops_role` takes `&ApiAuthed` and rejects job tokens.
`is_devops_email` is true for superadmin emails, so every worker-management,
instance-config and service-log route was reachable by the same superadmin
`WM_TOKEN` that `require_super_admin` already rejects.
- A `job_id` claim that does not parse as a uuid rejects the token rather than
resolving to `None`, which would clear the job provenance and uncap it. Applies
to the internal JWT and the external `jwt_ext_` path.
- Defense in depth at store time: `validate_on_behalf_of` refuses the reserved
internal sentinels as an `on_behalf_of` on apps/flows/scripts/schedules/triggers,
and app execution refuses a policy carrying one — covering already-persisted and
forked-app rows that predate the cap. Deploying on behalf of a real user,
including a real superadmin, stays allowed; the cap handles that at execution.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(mcp): preserve job-token provenance when minting the proxy JWT
The MCP endpoint-tool proxy re-mints a JWT from the caller's ApiAuthed to
forward the proxied request, but passed job_id: None. A job's WM_TOKEN is
capped at workspace admin (GHSA-hfh4-cx4h-3fcr); dropping the job_id here
re-minted an uncapped token that satisfies require_super_admin /
require_devops_role on the proxied route (e.g. listWorkers exposing worker
IPs, job/workspace IDs, and sensitive tags).
Carry api_authed.job_id into create_jwt_token. Adds an in-module regression
that decodes the forwarded JWT and asserts the job_id is preserved for a job
caller and absent for a non-job caller.
Reported by Codex CI review (P1) on #10124.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: cap the admin-or-devops gate at workspace admin for job tokens
require_admin_or_devops (the EE critical-alerts endpoints) grants when the
caller is a workspace admin OR an instance devops. is_devops_email is true
for superadmins, so a WM_TOKEN running on-behalf of a superadmin who is not a
member of the target workspace could clear the devops branch and read/ack that
workspace's critical alerts (GHSA-hfh4-cx4h-3fcr). This gate takes a bare
email, not an ApiAuthed, so the token-layer cap could not see it.
Thread the caller's job-token provenance and reject the devops branch for job
tokens, matching require_devops_role. The workspace-admin branch stays allowed
— that is the cap ceiling. Adds an enterprise-gated regression proving the
bypass is closed and a real superadmin token still clears the gate.
Found while auditing the PR for bare-email gates the choke-point cap misses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: cap instance-global is_admin gates at workspace admin for job tokens
Three instance-global routes gate on the caller's own `is_admin` claim, which
`ApiAuthed.is_admin` carries into a WM_TOKEN (it is a workspace-admin claim,
true for superadmins too). A job token is capped at workspace admin
(GHSA-hfh4-cx4h-3fcr), so its is_admin claim must not authorize instance
actions on a route with no workspace binding:
- `unarchive_workspace` — unarchive an arbitrary workspace by id
- `prune_concurrency_group` — delete a global concurrency group
- `list_worker_groups` — return unobfuscated `env_vars_static` (may hold secrets)
Add job-token-aware `is_instance_admin` / `require_instance_admin` helpers (the
same shape as `require_super_admin` / `require_devops_role`) and use them at
these three sites. Workspace-scoped `require_admin(authed.is_admin, ...)` gates
are intentionally left unchanged — a workspace-admin job token is within the
cap there. Regression added covering all three; verified it lets a WM_TOKEN
unarchive/leak without the fix and is blocked with it.
Reported by Codex CI review (P1) on #10124.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(mcp): drop orphaned path_field_renames from EndpointTool test helper
The merge with main adopted main's mcp path-substitution refactor (#10162),
which removed the `path_field_renames` field from `EndpointTool` and its
consumer (`substitute_path_params` no longer takes per-field path renames).
main's `runner.rs` `ep` test helper still constructed the struct with
`path_field_renames: None`, so the workspace test build (cargo test --all,
which compiles windmill-mcp's own #[cfg(test)] module under the `server`
feature) failed with E0560. A plain `cargo check` does not compile that test
module, so it only surfaced in CI's cargo_test.
Remove the orphaned field to match the struct.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test: describe the sentinel-rejection policy the forged-identity test asserts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: complete ApiAuthed initializers in feature-gated tests after merge
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: stop job tokens minting credentials that shed their provenance
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: cap the MCP OAuth approval mint at the same elevated-job-token gate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: cap the self-service password reset at the elevated-job-token gate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: cap app embed/SDK mints and scope widening at the elevated-job-token gate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: keep job tokens from destroying the account they run on behalf of
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: deny job tokens a foreign-workspace admin claim and workspace ejection
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: keep the follow-up inventory in the PR instead of the repo
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: make the session workspace status gate job-token aware
session_workspace_status derived its superadmin branch from a bare email
check, so a job token carrying a superadmin identity resolved the existence
of workspaces it has no relationship with rather than seeing them as
deleted. Switch to is_super_admin_authed, matching every other instance
gate reached from a request ApiAuthed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* revert: leave the global concurrency-group listing on the plain admin gate
The listing exposes concurrency keys across workspaces, which is metadata
rather than a capability, and it 401s rather than degrading. Keep the guard
on the prune route next to it, which is the destructive one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: keep the instance-admin gate on the global concurrency listing
The listing spans every workspace's concurrency keys, and the gate rejects
only job tokens: the !is_admin branch is the pre-existing check, so
workspaced tokens and interactive admins are unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to d30af67d38954f9012f7bad08da23e347344b4c6
This commit updates the EE repository reference after PR #664 was merged in windmill-ee-private.
Previous ee-repo-ref: 7870573dbc3360f99bada143f094c67dce0d9e9c
New ee-repo-ref: d30af67d38954f9012f7bad08da23e347344b4c6
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: hugocasa <hugo@casademont.ch>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
|
||
|
|
fb82748296 |
fix: make on_behalf_of control permissions for scripts and flows (#10438)
* fix: make on_behalf_of control permissions for scripts and flows Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: inherit the recorded on-behalf-of identity when a preserving deploy omits it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep an omitted permissioned_as from re-versioning an unchanged script Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: derive the on-behalf-of principal from the email and reject mismatched pairs Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: stop workspace deploys from carrying a source-workspace principal Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: correct the onBehalfOfPermissionedAs param doc Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test: pin that workspace deploys never carry a source-workspace principal Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: correct the omitted-principal contract and refresh generated prompts Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep external-superadmin principals on email-only redeploys Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: scope the recorded principal to its workspace and prefer real accounts Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: carry the recorded principal correctly through drafts and set-permissioned-as Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: sweep draft identity pairs on email change and offboarding Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: leave group identities alone when sweeping a user's email Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: treat only g/ without an email as a group, and match the offboard preview Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: stop the group guard from skipping rows with no recorded principal Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: state the group guard once instead of restating it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: make the permissioned_as the only stored on-behalf-of identity Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * perf: skip resolving the on-behalf-of address for sync clients that discard it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: address the local review of the identity refactor Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: resolve the on-behalf-of identity coherently across clones, offboarding and no-op deploys * test: pin that a fork keeps only the on-behalf-of identities that resolve in it * fix: decide a principal prefix-first everywhere and canonicalize bare addresses * fix: prefix a slash-containing address so a reader cannot take it for a group * fix: read an address as a username before the group- convention * fix: rewrite the canonical principal when an account's address moves * fix: keep the address form of a principal to accounts without a usr row * fix: reject an identity a job row cannot carry and read it uncached at dispatch * fix: count characters against the job identity width and cap the backfill * refactor: name the script/flow principal on_behalf_of, as apps do * docs: state the caller-must-authorize contract on the identity resolvers * fix: keep writing on_behalf_of_email until every worker reads the principal * fix: err high on the compatibility version and document the last resolver * fix: keep the compatibility address current through identity mutations * fix: carry the compatibility address with the principal on every copy path * chore: re-pin the EE ref to the companion branch merged with EE main * fix: key the dbt retry lookup on the stored principal * fix: keep a mixed-version address recoverable through a fork * fix: read a round-tripped address uncached so a redeploy is not rejected * fix: refuse an email change that would make a principal unenqueueable * chore: update ee-repo-ref to ac3d7d015296f041ae44ab6bc4953485f44d36e4 This commit updates the EE repository reference after PR #704 was merged in windmill-ee-private. Previous ee-repo-ref: 219b0b03905a1a0028054b3a4985724e77d09036 New ee-repo-ref: ac3d7d015296f041ae44ab6bc4953485f44d36e4 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> |
||
|
|
68debab877 |
feat(triggers): add AMQP (RabbitMQ) trigger via lapin (#10230)
* feat(triggers): add AMQP (RabbitMQ) trigger using the lapin library Fixes WIN-2214 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(triggers): defer AMQP cross-workspace deploy pending utils-internal publish Revert the amqp_trigger additions to the shared windmill-utils-internal TriggerDeployKind and the frontend cross-workspace deploy adapter: the frontend installs the published npm package, which lacks the new kind until a release is cut. AMQP create/edit/delete/list/sync/capture are unaffected (they use local types); only cross-workspace deploy/merge of AMQP triggers waits on the package bump. Also document the at-most-once ack in the consumer loop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(triggers): address AMQP review — at-least-once ack, workspace cascade, contracts - ack AMQP deliveries only after successful dispatch; nack+requeue on failure - add ON DELETE CASCADE workspace FK so amqp_trigger rows are cleaned on workspace deletion (and the listener stops) - fix the /amqp_triggers/test OpenAPI body and add amqp_trigger to WorkspaceDiffRow.kind - register AMQP in the generated workspace trigger tool (create_trigger) - drop banned $bindable defaults on optional props in the config section - add build_uri unit tests (encoding, ports, vhost) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(triggers): stop AMQP poison-message loop and reconnect on transient drops Chaos testing against a live RabbitMQ broker showed the previous nack(requeue) + immediate re-poll spun a tight redelivery loop (~1000 critical-error reports/sec) on a poison message, and any connection blip permanently disabled the trigger (lapin has no built-in reconnect). - on dispatch failure: nack+requeue then stop consuming; the listener framework re-lists the trigger after its ping goes stale (~15s), backing redelivery off to that cadence instead of a tight loop (verified: rate dropped from ~1000/s to ~1 per ~26s, message preserved) - on connection/stream error: stop and let the framework reconnect instead of disabling; persistent failures are still disabled via get_consumer (verified: a forced connection close now auto-reconnects and resumes) - finish the AI create-trigger action wiring for AMQP: add amqp to CreatedResourceTriggerKind, the action-card registry, and the drawer registry so the result card renders and its "Open" action works Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(triggers): complete AMQP frontend registries and defer merge rows - add amqp to capturableTriggerTypes (so AmqpCapture mounts), the Runs jobTriggerKinds filter, and CLOUD_DISABLED_TRIGGER_TYPES - wire AMQP into global AI chat mode: TRIGGER_KINDS, the request union, writeTriggerSchema, triggerServices, and the draft adapter - stop emitting actionable AMQP fork-comparison rows (revert amqp_trigger from TRIGGER_OR_SCHEDULE_TABLES) since cross-workspace deploy is deferred until windmill-utils-internal is published — avoids a deploy that fails with "Unknown kind: amqp_trigger" - use design-system TextInput instead of raw <input> in the config section Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(triggers): complete AMQP session/draft registries and constrain prefetch - add amqp to the session-deploy, draft-compare, preview-router, and copilot workspace-item registries so AMQP drafts/deploys/nav/path resolution work - include amqp_count in the MoveDrawer attached-trigger rename warning - replace the raw prefetch <input> with a design-system TextInput bounded to an integer 1-65535 (backend u16) and block save on invalid values Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(triggers): make AMQP disconnect/reconnect consistent with the Kafka trigger lapin, like rdkafka, has no transparent reconnect, so the AMQP listener now mirrors the Kafka trigger's explicit reconnect loop instead of relying on the framework re-list (which disabled the trigger once get_consumer failed on a sustained outage): - get_consumer returns cheaply; consume owns a (re)connect loop that retries with a 30s backoff, reports a critical error every 10 failed attempts, and reports a recovered critical error once it reconnects — never disabling the trigger on a connectivity failure - a consumer/stream error breaks out to reconnect rather than disabling - dispatch failure still nacks+requeues (at-least-once) with a short backoff to avoid a tight poison-message loop, keeping the connection alive Verified against a live RabbitMQ broker: killing the broker keeps the trigger enabled and retrying (attempt N), and restarting it auto-reconnects (logs "reconnected after N attempts") and resumes dispatch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(triggers): complete AMQP capture registries and constrain prefetch contract - add the 'amqp' case to triggerKindToTriggerType so opening the AMQP editor from a capture button no longer throws "Unknown TriggerKind: amqp" - register AmqpIcon in CaptureTable's icon map and add an AMQP entry to the script/flow CaptureButton menu - bound the OpenAPI prefetch_count to an integer 1-65535 (matches the Rust u16) and regenerate clients/prompts - require a non-empty exchange name when the exchange binding is enabled - build_uri: fall back to "/" on a blank vhost and bracket IPv6 hosts (+ tests) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(triggers): wire AMQP into pipeline graph, git-sync, and preprocessor types - asset_graph: discover attached amqp_trigger rows and emit an AMQP TriggerEdge so AMQP triggers render (and can be opened/deleted) on the data-pipeline canvas - frontend pipeline graph: add amqp to NativeTriggerKind, the add-trigger menu, node presentation, event-trigger set, annotation keywords, and the editor/service registrations - git-sync: add the amqp_trigger include pattern (+ test) so an AMQP git-sync deployment stages only its .amqp_trigger.* file, not an unrelated same-path object - preprocessor starters: add the AMQP event to the generated TS/Python/PHP trigger event types (kind/payload/exchange/routing_key/queue_name/redelivered/ delivery_tag) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(triggers): finish AMQP pipeline/parser wiring, prefetch validation, source lists - fix a stray edit that corrupted the pre-existing MqttTriggerEditor import ($lib/... path) in PipelineTriggerEditors.svelte - reject prefetch_count = 0 server-side in validate_config (RabbitMQ treats 0 as unlimited) and defensively skip basic_qos(0) in build_consumer (covers the capture path that bypasses CRUD validation) - recognize `// on amqp` in the canonical parser (TriggerSpec::Amqp) and add amqp to the CLI non-autorun/event-trigger sets so a pipeline cascade never runs an AMQP-only node as a manual root without an event - add amqp to the preprocessor intro lists and both pipeline AI instructions Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(triggers): reject zero AMQP prefetch in all paths and finish guidance lists - extract a shared validate_amqp_options used by both CRUD validate_config and build_consumer, so capture configs (which bypass CRUD validation) also reject prefetch 0 instead of silently connecting with an unlimited buffer (+ unit tests for 0/1/65535/None) - add AMQP to the main script-writing preprocessor-sources prompt and the CLI triggers-skill guidance list Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(triggers): de-duplicate AMQP prefetch comment and fix GET response text - keep the zero-prefetch rationale only on the shared validate_amqp_options doc; drop the redundant call-site comments - correct the getAmqpTrigger OpenAPI 200 description ("deleted" -> "retrieved") Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to aaa6cb89b05b76139252c64f057e53b94d12ac60 This commit updates the EE repository reference after PR #680 was merged in windmill-ee-private. Previous ee-repo-ref: 5da5fd65aca9594b2611837a52e4677b544b0380 New ee-repo-ref: aaa6cb89b05b76139252c64f057e53b94d12ac60 Automated by sync-ee-ref workflow. * chore(migrations): consolidate the four AMQP migrations into one The table and the three enum ADD VALUE statements (trigger_kind, job_trigger_kind, draft_kind) are one atomic feature. ALTER TYPE ... ADD VALUE runs inside the migration transaction on PG >= 14 (Windmill's minimum) since the amqp_trigger table doesn't reference those enum types, so they can share a single migration instead of four. Verified applying cleanly in a single transaction on a fresh DB. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> |
||
|
|
043c2c05b7 |
fix: forbid superadmin job tokens from global user and token management (#9715)
* fix: forbid superadmin job tokens from global user and token management Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: extend superadmin job token guard to offboard and export routes Apply forbid_superadmin_job_token to offboard_global_user and export_global_users, the remaining global user-management routes that were gated only by require_super_admin. Offboarding can delete a user along with their tokens, password, invites and instance-group membership, and export returns every user's password_hash, so both must be unreachable by a superadmin job token. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1fc355709c |
feat: Db-backed user drafts (#9351)
* Db draft removal * refactor: drop unsaved-changes confirmation modal from editors * fix: remove nodraft from flow row edit link * fix: remove nodraft from app and raw app edit buttons * fix: remove nodraft from all edit links * fix: merge backend defaults into legacy autosaves to avoid spurious restore toast on raw apps * feat: add username column to draft table for user-scoped drafts * feat: add sync_drafts and list_users_with_draft_on_path endpoints * feat: add UserDraftDbSyncer service for bi-directional draft sync * feat: wire UserDraft.save through DbSyncer + conflict modal * refactor: gate useLocalStorageValue nested-update effect behind opt-in flag * refactor: move sync force flag from request-level to per-entry * feat: sync all userdraft kinds, switch draft owner to email FK, add id PK, scope draft list to readable paths * refactor: route draft permission check through authed.folders + RLS, drop client-supplied email * feat: support draft deletion via sync (value: null) with same conflict semantics * feat: surface other users' drafts in editors with diff+fork action * refactor: unify draft schema migrations and type kinds via DRAFT_KIND enum * perf: add (workspace_id, email, created_at) partial index for sync hot path * chore: update ee-repo-ref to a30079e75dc5b7d7413aa8ee20e40e80bfea9cbd This commit updates the EE repository reference after PR #597 was merged in windmill-ee-private. Previous ee-repo-ref: 55c19293232be379a3044eb78f677b545882ffd6 New ee-repo-ref: a30079e75dc5b7d7413aa8ee20e40e80bfea9cbd Automated by sync-ee-ref workflow. * fix(userdraft): trigger sync on deep mutations via readFieldsRecursively * Rollback UserDraft * remove queuing logic * pushDrafts * refactor: remove draft sync layer and conflict modal * feat: add save_draft, list_drafts, get_draft routes * feat: add get_draft overlay to getScriptByPath * feat: extend get_draft overlay to flow, app, resource, variable, schedule, triggers * feat: support null value in save_draft for deletes * readLastSyncMap * feat: redirect /add pages to /edit/draft_uuid with new_draft flag * fix: inline get_draft query field instead of flattening * fix: drop dangling nobackenddraft assignment in flows edit * feat: include user drafts in list endpoints with is_draft flag * fix: prefix draft paths with u/{user} and seed editor state on new_draft * fix: route draft-only deletes through UserDraftDbSyncer on home page * feat: delete user drafts when their underlying item is deleted * fix: empty path seed on new_draft so friendly auto-name fires * feat: re-add Draft and Draft only badges on home page rows * fix: synthesize value wrapper on draft-only raw_app response * fix: tolerate missing latest-version on draft-only flow reload * fix: skip first observable change in DB sync effect to match LS persist * fix: remove URL-hash sync from script editor (already marked TEMP) * refactor: drop localStorage layer from UserDraft * refactor: drop vestigial LS-era code from UserDraft * feat: migrate localStorage drafts to DB on layout mount * fix: migrate session runtime + script view to per-user draft API * feat: add 'Reset to deployed' action on draft-loaded toast * feat: hide 'Reset to deployed' action when no deployed version exists * createCoalescingKeyedRunner * example ts doc * createDebouncerByKey * refactor: drop await on draft-delete in reset flows, refetch deployed directly * fix: bridge saved-draft shape to wire shape in apps/resources/variables loaders * feat: route UserDraftDbSyncer.save through debouncer + coalescing runner * feat: add immediate-save bypass that cancels pending debouncer + runner tasks * fix: seed UserDraft cell from spec defaultValue on acquire * fix: redirect /add routes at load phase to eliminate white flash * fix: drop +page.js files in /add routes that conflicted with +page.ts * refactor: send draft as separate .draft field instead of deep-merging onto deployed * feat: surface draft path in home list when user typed one different from URL * feat: add UserDraft.stopSync/restartSync, wire on script + low-code app /add init * fix: thread URL path into ScriptBuilder.stopSync (was using empty initialPath) * fix: also stopSync in route's new_draft branch + queue pre-acquire suspensions * feat: add AutosaveIndicator backed by reactive UserDraftDbSyncer.getState * refactor: drop draft-loaded toast in non-route editors, banner now compares draft vs deployed * fix: gate per-user draft-only rows in listings on include_draft_only flag * feat: flush pending draft saves via keepalive fetch on tab hide / pagehide * autosave indicator nits * fix: route create-vs-update on /add deploys; seed policy.execution_mode; sync script template * chore: add [draft-sync] console logs to trace script bootstrap autosave * fix: seed auto-generated path in script new-draft route to suppress Path widget's autosave-triggering mutation * fix: defer script restartSync until script.path lands (Path widget gated on $userStore + $workspaceStore) * fix: poll script.path via tick() until Path widget settles before restartSync * chore: log inferArgs underlying error on deploy to diagnose 'Could not parse code' toast * fix: wait for script.path to stabilize across two ticks before restartSync * revert: drop unsuccessful path-stabilization heuristics + leftover [draft-sync] logs * fix: seed new-draft script schema as emptySchema() so inferArgs doesn't trip on undefined properties * fix: heal legacy drafts with schema={} (no .properties) on deploy * autosave indicator * refactor(editors): drop UnsavedConfirmationModal mount + Show diff button * feat(drafts): collaboration banner, cross-tab conflict detection, raw app template picker - Other-users-drafts banner (Modal2): the deployed-overlay response now carries `other_drafts_users` (workspace usernames only, never emails); each row offers View JSON + Fork. Drops the standalone `listUsersWithDraftOnPath` endpoint; `getDraftForUser` now takes a workspace `username` query param (resolved to email server-side). - Cross-tab/browser save conflict detection: the syncer attaches `last_sync` to every save (defaults to non-force); on a `conflict` response it parks a snapshot in a reactive map. Each route mounts a `DraftSyncConflictModal` and seeds the per-tab `last_sync` via `recordRemoteSync(query, draft_saved_at)` on every `get_draft` load. Keepalive flush also respects optimistic concurrency. - Raw app template picker re-added after the /add ⇒ /edit refactor: framework (React 19 / 18 / Svelte 5), data table + schema config, and optional AI prompt — extracted into `RawAppTemplatePicker.svelte` and driven by `new_draft=true` on the edit route. * fix(drafts): suppress autosave during /add template seeding on script + raw app editors - ScriptBuilder: delay `restartSync` 500ms past `initContent` + stores- ready so the Path widget's `$workspaceStore && $userStore`-gated `initPath → reset → onMetaChange → bind:path` cascade lands inside the suspension window. Two `tick()` waits weren't enough — the bind:path mutation fired ~100ms after the prior `restartSync` and posted as a "user edit". - apps_raw route: suspend autosave on `new_draft=true` and resume only after the framework picker closes (via `onStart` or X dismissal), with a two-tick settle so the picker's seeded `files/runnables/data/policy` mirror to `draftHandle.draft` observably advances `lastSerialized` before sync re-arms. * fix(drafts): land /add redirects on the real workspace username, not "me" The `/add` → `/edit/u/{username}/draft_{uuid}` redirects ran during SvelteKit's load phase, BEFORE the (logged) layout's async `getUserExt` populated `userStore`. `get(userStore)?.username` returned undefined and fell back to the `'me'` placeholder on every fresh nav, producing `u/me/draft_{uuid}` paths instead of the user's real namespace — broke ownership checks against `authed.username` and silently scoped autosaves under the wrong path. Layout now persists `username` to localStorage on every successful `getUserExt`, and `getUsernameForNamespace` (new shared helper, used by all four `/add/+page.ts` files) reads the live store first, falls back to the cached value, and only then to `'me'` for true first-ever loads. * fix(drafts): key low-code app autosave on the URL path, not the empty string `AppEditor` keyed its `UserDraft.use` handle on `newApp ? '' : path` — a legacy leftover from when `/apps/add` was its own URL (no path). With the `/add` ⇒ `/edit/u/{user}/draft_{uuid}` redirect, `newApp=true` made autosaves land on the `('app', '')` row instead of the URL path: - The `apps/list?include_draft_only=true` query joins drafts onto `app.path`, surfacing drafts at the URL path. The empty-path row didn't match the user's URL so the draft never appeared in the home list. - Refreshing `/apps/edit/u/{user}/draft_{uuid}` re-fetches at the URL path with `?get_draft=true`, finds nothing, and 404s. Drop the ternary so the handle always uses `path` — the same as scripts/flows/raw_apps. The route's `?new_draft=true` branch already seeds the empty-template baseline, so there's no longer a "the draft sits under '' until first save" race to worry about. * fix(raw_app): propagate template picker X / Esc dismissal so autosave resumes The picker mounted `<Modal kind="X" open ...>` (one-way prop, not `bind:open`). When the user dismissed via X / Esc / click-outside, the inner Modal flipped its own local `open` to false (hiding the UI) but never wrote back to the picker's `open` $bindable. The route's `templatePicker → false` watcher — the one that calls `restartSync` two ticks after the picker closes — never fired, so autosave stayed suspended and the user's edits after dismissal were silently dropped. Switch the inner Modal to `bind:open` so the dismissal bubbles all the way up to the route's state. "Start without AI" already worked because its `onStart` handler explicitly sets the picker's `open = false`. * nit unused * fix(drafts): make the home-page View/Edit JSON action work on draft-only apps The "View/Edit JSON" entry on the home page called `AppService.getAppByPath` without `get_draft=true`, so for draft-only items at `u/{user}/draft_{uuid}` the backend 404'd with "App not found at path …". Pass `get_draft=true` and render the synthesized stand-in's editable shape: - App drafts come back as `{summary, value, path, policy, ...}` — `value` is the App definition the editor was working on; show that. - Raw-app drafts come back as the flattened `{files, runnables, data, summary, policy, ...}` with no nested `value`; show the whole shape. On save, draft-only items can't go through `updateApp` (no deployed row). Route the edit through `UserDraftDbSyncer.save` (with `immediate: true` so `await` resolves after the POST lands) and relabel the button "Save draft" + Save icon. Deployed items keep the existing "Deploy" flow unchanged. * fix(drafts): render the right shape in View/Edit JSON for draft-only items The previous fix landed `fapp.value` into the editor, but the deployed-overlay flattens the bare editable shape into `inner`/the top-level response — drafts have no nested `.value`. So: - App drafts (`{grid, breakpoints, hiddenInlineScripts, …}`) rendered as empty (`fapp.value` was undefined). - Raw-app drafts 404'd outright: `get_draft=true` with no `rawApp` flag can't tell which draft kind to look up, defaults to `app`, doesn't find one. Thread the row's `raw_app` flag from AppRow → `appExport.open(path, rawApp)` → `getAppByPath({..., rawApp})` so raw-app drafts resolve to the right `UserDraftItemKind`. Read `fapp.draft` (the bare editable shape from `fetch_draft_only`) into the JSON editor for draft-only items — clean payload, no `is_draft` / `no_deployed` / overlay noise. Save the same bare shape back through the syncer so the regular editor reads it unchanged on the next mount. * fix(drafts): skip public-secret-URL fetch in the Deploy drawer for draft-only apps Opening the Deploy drawer on a `/edit/u/{user}/draft_{uuid}` app fired `AppService.getPublicSecretOfApp` immediately because the gating effect only checked `appPath != ''` + `savedApp`. The `/secret_of/{path}` route plain-SELECTs `app.id`, so a draft-only path 404'd with "App not found at name …" and the public-URL ClipboardPanel spun forever waiting on `secretUrl`. Thread the existing `newApp` signal (already on `AppEditorHeader` / `RawAppEditorHeader`) into `AppEditorHeaderDeploy`, gate the fetch behind `!newApp`, and render the existing "Deploy this app once to get the public secret URL" placeholder instead of the spinner for draft-only items. * fix(drafts): disable Diff button on draft-only items across the 4 editors Diff has no baseline to compare against on draft-only items — the button used to be gated by the pre-PR `/add` route's own state, but the `/add → /edit` redirect landed everything under the regular `/edit` page where the gate was missing. - ScriptBuilder: gate the topbar Diff on `savedScript.no_deployed`; seed `no_deployed: true` on the route's `new_draft` empty NewScript so the gate fires before the first deploy. - FlowBuilder: gate the topbar Diff on `newFlow` (route already sets it from `backendFlow.no_deployed` and the new-draft branch). - AppEditorHeader: gate both the "Diff" dropdown action and the Deploy-drawer's "Diff" button on `newApp`. - RawAppEditorHeader: gate the topbar Diff + the Deploy-drawer's "Diff" button on `newApp`. Each gate also rewrites the tooltip ("Deploy this … once to compare against the deployed version") so the hover state explains why. * fix(drafts): disable the "No login required" toggle on draft-only apps Flipping the toggle called `setPublishState`, which POSTs the new `policy` through `AppService.updateApp` — that handler's `UPDATE app ... RETURNING path` finds nothing on a draft-only path and `not_found_if_none` 404s with "App not found at name …" (apps.rs:1975). Gate the Toggle on `!newApp` too so the user has to deploy once before configuring the publish state. * refactor(drafts): drop dead draft_path field from list responses The draft-only listing branches in scripts/flows/apps computed a `draft_path` from the draft JSON (when the user-typed path differed from the URL's autogenerated `u/{user}/draft_{uuid}`), and `{Script,Flow,App} Row.svelte` preferred it over `path` for the row title. In practice that path is never written: the app, raw-app and flow editors all warn "Deploy the X to make the path change effective" — the rename only lands on deploy, never in the draft. So the field is always None and the home rows always show the autogenerated slot anyway. Drop the field from the three `Listable*` structs, the three draft-only push sites, the three OpenAPI response schemas, and the three frontend row components. Client regenerated. * fix(drafts): seed a friendly name on /flows/add The flow route passed `initialPath={page.params.path ?? ''}` to FlowBuilder, so on the `/flows/add → /flows/edit/u/{user}/draft_{uuid}` redirect the Path widget's `initPath` saw a non-empty `initialPath` and skipped the `reset()` branch that auto-generates the friendly `<random_adj>_flow` name. The other three editors all clear `initialPath` in their `new_draft` branch for exactly this reason. Track `initialPath` as route-owned state (defaults to the URL path) and clear it to '' inside the `new_draft` branch, then bind it through to FlowBuilder so any post-deploy update from the editor still propagates. * feat(drafts): render friendly user-typed path on home list for all 4 kinds Reinstate `draft_path` on `Listable{Script,Flow,App}` so the home rows prefer the user-typed name over the autogenerated `u/{user}/draft_{uuid}` URL slot, with two source rules — one per how each editor wires the Path widget: - Scripts already work: `ScriptBuilder` binds the Path widget directly to `script.path`, so the typed path round-trips through the draft JSON's own `path` field. Backend extracts `v["path"]` when it differs from `row.path`. - Flows / apps / raw apps don't write the typed path into the autosaved value (`Flow.path` is one-way-bound to `$pathStore`; the bare `App` / raw-app value has no `path` field at all). Introduce an explicit `draft_path` field on the draft JSON, written by the editor ONLY when the typed path differs from the deployed/seeded `savedX.path`: - FlowBuilder: $effect on `$pathStore` mutates `flow.draft_path`. - AppEditorHeader: $effect on `newEditedPath` mutates `$app.draft_path`. - RawAppEditorHeader: $effect surfaces `pendingDraftPath` up via the bind chain (RawAppEditor → route); the route's draftHandle.draft spread includes `draft_path` when set. Backend extracts `v["draft_path"]` and `None` when unchanged or after deploy (deploy clears the whole draft, so the field naturally disappears post-deploy without bookkeeping). Flow route's `new_draft` branch now stops sync around the Path widget cascade, with a 700ms scheduled `restartSync` (mirrors the existing scripts/apps/raw_apps stoppers) — the new draft_path mutation lands inside that window so `/flows/add` no longer fires an autosave before the user's first edit. openapi/sqlx regenerated. * fix(drafts): preserve the user-typed draft_path on reload of draft-only items The flow / app / raw-app editors all dropped the saved `draft_path` back to the URL's `u/{user}/draft_{uuid}` slot the moment the user reloaded a draft-only edit page: the route sourced the Path widget's initial path from `page.params.path` instead of the previously-saved `draft_path`, and the first user edit then mirrored that URL path back into the autosaved draft — silently overwriting the friendly name in both the row and the editor. - Flow route: after computing `effectiveFlow`, override `flowInitialPath` with `effectiveFlow.draft_path` when set. - App route: pass `newPath={(app.value as any)?.draft_path ?? app.path}` through to `AppEditor`; AppEditorHeader's `newEditedPath` default now prefers a non-empty `newPath` over the random `<adj>_app` seed (the `newApp && !newPath` branch keeps the `/apps/add` friendly auto-name). - Raw-app route: surface `savedRawAppDraft.draft_path` onto `backendApp` so the `extractRawApp` path seeds `newPath` with the friendly name. Reload + a subsequent edit now leaves `draft_path` intact for all three kinds; verified end-to-end via the `/drafts/get_draft/...` endpoint. * fix(ui): default Modal2 target to 'body' so omitting the prop doesn't throw Modal2 defaulted `target = ''` and forwarded it to `Portal`, which calls `document.querySelector(target)` — an empty selector throws "Failed to execute 'querySelector' on 'Document': The provided selector is empty" and the modal silently fails to mount. That's why `OtherUsersDraftsModal` (and `DraftSyncConflictModal`) never appeared on editors where another user had a draft — both omit the `target` prop. Other Modal2 callers (StorageSettings, CriticalAlert, CustomInstanceDbWizardModal, …) pass an explicit `target="#content"` and were unaffected. Match Portal's own default of `'body'` so omitting the prop is now a no-op rather than a runtime throw. * fix(drafts): Reset to deployed no longer resurrects the draft The toast's "Reset to deployed" callback POSTed `value: null` to the syncer, then handed control to the route's `onResetToDeployed` (which wipes the in-memory handle and reloads the deployed payload via `getDraft: false`). Both writes flowed through the reactive sync effect: the wipe scheduled a delete, the reload scheduled a re-save of the deployed value as the new draft. Coalescing collapsed them and the draft came back — making the "discard" action effectively a no-op. Wrap the whole callback in `UserDraft.stopSync` / `restartSync`. The explicit `value: null` POST still goes through (it's a direct `UserDraftDbSyncer.save` that doesn't depend on the reactive effect), the route's wipe-then-reload mutations advance `lastSerialized` silently under suspension, and the next user edit (after two ticks past the deployed-seed write) is the first real save again. * ui nit * feat(drafts): autosave-indicator popover with Reset-to-deployed action Click the cloud icon → popover with "All changes are saved as a draft on the server. The draft is per-user — your teammates' editors keep their own." When the editor isn't on a draft-only path AND the user has a draft (UserDraft.has returns true), a "Reset to deployed" button mirrors the load-time toast action — stops sync, POSTs `value: null`, runs the route's reload-without-draft callback, restarts sync past two ticks so the deployed-seed write doesn't resurrect the draft. Threaded `onResetToDeployed` from each route down to its builder (ScriptBuilder / FlowBuilder / AppEditorHeader / RawAppEditorHeader) and into the indicator. `draftOnly` is wired from `savedScript.no_deployed` / `newFlow` / `newApp` so the action hides where there's nothing to fall back to. The indicator's trigger now has a hover affordance + matches Portal's default target ('body') via Modal2's earlier fix. * fix(drafts): wait for the fork POST to land before navigating OtherUsersDraftsModal's Fork action called UserDraft.save, which routes through the autosave debouncer (1500ms). The subsequent goto fired within the same tick, so the destination editor's get_draft=true read ran before the POST landed and 404'd — refreshing worked because by then the debounced save had fired. Call UserDraftDbSyncer.save with immediate: true and await it. The syncer cancels any queued debouncer task for the key and resolves the promise only after the POST completes, so the route load can find the forked draft on the first try. * fix(drafts): conflict detection — keep last_sync map tab-local instead of in localStorage Two tabs editing the same draft both load with last_sync = T0. Tab-1 saves; the server accepts, returns T1, and the syncer wrote T1 into localStorage. Tab-2 then tries to save: it reads the SHARED localStorage map, sees T1 instead of its own baseline T0, sends last_sync = T1, and the backend's WHERE clause (`created_at <= last_sync`) is true → tab-2 clobbers tab-1's edit without ever seeing a conflict. Move the map to tab-local memory (`new Map<string, …>`). Reload of the tab now starts with an empty map; that's fine because the editor's load path calls `recordRemoteSync(query, draft_saved_at)` right after `get_draft=true` returns, reseeding from the authoritative server timestamp before any user edit could fire a save. * fix(drafts): OtherUsersDraftsModal — close on Fork, don't leak clicks through nested JSON Two bugs in the per-editor "another user has a draft" banner: - Fork landed the immediate save but didn't close the banner before navigating. Svelte hadn't torn down the previous route's components by the time goto returned, so the banner lingered on top of the destination editor. Comment the explicit isOpen=false on the happy path so it's clear it MUST run before goto. - Clicking anywhere on the screen while the View JSON drilldown was open closed the underlying banner too. Modal2's clickOutside action fired on every Modal2 instance — both the JSON modal and the underlying banner — because both attach their own listener at the document level. Add `closeOnOutsideClick` opt-out on Modal2 and pass `closeOnOutsideClick={!jsonOpen}` to the outer modal so clicks outside the JSON drilldown only close the drilldown. Drive-by: Modal2's keydown handler now ignores Escape when its own isOpen is false (was a no-op closer that would still preventDefault on every key press, swallowing key events for any siblings). * fix(drafts): conflict modal wording — drafts are user-scoped, not teammate-scoped * fix(drafts): defer reset-to-deployed restart until first user interaction Two-tick `restartSync` was too aggressive: editor remounts emit a tail of cascading writes (Monaco setValue acks, schema re-infer, UI Builder iframe handshakes, schedule-config recomputes, …) that land well after two ticks and would clobber the just-deleted draft with an upsert of the deployed value — making "Reset to deployed" a no-op in practice, the user kept seeing the draft come back. Centralise the suspension lifecycle in a new `runResetToDeployed` helper. It stopSyncs around the reset, POSTs the explicit delete, runs the route's wipe-and-reload, and then arms a one-shot listener on document keydown / input / pointerdown that restartSyncs on the user's next real interaction. A 5-second fallback re-arms sync if the user walks away without touching the editor, so suspensions don't leak. Use it from both the load-time toast (`notifyDraftLoaded`) and the autosave-indicator popover so the two stay in sync — fixes both entry points. * indicator ui nits * fix(drafts): split tab-switch and unload flushes — kill self-conflict on visibility change The single keepalive flush bound to both `visibilitychange → hidden` and `pagehide` self-conflicted on tab switch: visibilitychange fires on every tab/app switch with the page still alive, the keepalive POST advanced the server's `created_at` to a fresh `now()`, the client discarded the response (no listener), the local `lastSync` stayed at the old value, and the next foreground autosave sent that stale timestamp → server saw `created_at > last_sync` → conflict modal for the user's own background-tab write. A still-pending debouncer task made it worse: it fired a second runner POST after the keepalive with the same stale `last_sync`, the second self-conflicted too. Split into two paths: - `visibilitychange → hidden` → `flushOnVisibilityHidden`: route through the normal runner pipeline. The page is alive, so the response can land and `setLastSync` keeps the baseline current. Call `debouncer.cancel(key)` first so a queued keystroke can't double-fire with the same stale `last_sync`. - `pagehide` → `flushOnPageHide`: keep the `keepalive: true` raw fetch for the genuinely-going-away case (the JS context is torn down, the response is necessarily discarded). Same `debouncer.cancel(key)` guard. On the next mount, the route's `recordRemoteSync(query, draft_saved_at)` reseeds `lastSync` from authoritative server state before any user edit can fire a save. * fix(drafts): drop the visibilitychange flush — debouncer keeps running on hidden tabs Tab switching just hides the page; the JS context survives and the debouncer's `setTimeout` keeps counting down. When it fires, the runner POSTs normally and the server's response updates `lastSync`. There's nothing left for a visibilitychange-driven flush to do that the ordinary pipeline doesn't already handle, and adding one only creates extra POSTs to reason about. `pagehide` remains the single trigger for the keepalive flush — that's the case where the JS context is actually being torn down and the runner's pending fetch would otherwise be killed mid-flight. * nit * refactor(drafts): drop LS-era pipeline; backend is canonical on load The PR's iteration left behind a meta/staleness pipeline carried over from the localStorage era — per-rev tracking, a LocalDraftStaleModal, a 'Restored from local storage' toast, and a localDraft-vs-backend comparison branch in every editor loader. With drafts now living in the DB and the optimistic-concurrency lastSync check handling divergence, that whole stack is dead weight. Worse, the comparison branch caused 'Load from server' in the conflict modal to do nothing: the loader preferred the in-memory cell over the backend, so the user-clicked 'load from server' just re-displayed the local edits AND fired two confusing toasts (Restored from local storage + Loaded your saved draft). The rip: * userDraft.svelte.ts: drop UserDraftMeta, StoredDraft.meta, checkStaleness, UserDraftStalenessCause, normalizeForCompare, localDraftDiffers, saveMeta, getMeta, setDraftAndMeta, setMeta, handle.meta/setDraftAndMeta/setMeta, force option. Handle is now just { draft }. * userDraftToast.ts: drop notifyRestoredFromLocal + RestoreFromLocalActions. Update copy. * LocalDraftStaleModal.svelte: deleted. * AppEditor.svelte: drop initialRevs prop and the firstMirror wipe-then-restore dance (it existed only to consume the meta-mismatch skip slot). * All 4 editor routes: backend is canonical on load — the in-memory cell is overwritten with the deployed+draft overlay, the syncer's seed guard swallows the first write so we don't POST it back. * VariableEditor / ResourceEditor: drop the staleness pipeline + rev bookkeeping; backend wins on open. * useTriggerDraftSync.svelte.ts: inline the JSON-normalize + deepEqual utility as a private cfgDiffers helper (kept for the form-vs-deployed dirty check, which is a genuine semantic compare, not LS legacy). * copilot core.ts / userDraftAdapter.ts: drop meta argument from saveAppDraft, loadAppDraftValue, write*Draft. Test assertions on getMeta dropped. Net: -22 typecheck errors, fewer moving parts, conflict modal works. EOF ) * refactor(drafts): remove dead endpoints + UserDraftDbSyncer.getLastSync The list_drafts and get_draft (own) routes were added during PR iteration and never wired up to any frontend caller — the editor overlay path uses the per-kind get-by-path getDraft query parameter, and the home page lists drafts via the per-kind list endpoints, not via /drafts. Drop both routes (+ sqlx caches + OpenAPI entries). UserDraftDbSyncer.getLastSync was a peep-hole for callers that never materialised — the per-tab lastSync map is only ever read by postSave internally, where the bookkeeping already lives inline. * refactor(drafts): extract DraftEditorModals trailer block The four editor routes (scripts/flows/apps/apps_raw) mounted an identical pair of trailer modals — DraftSyncConflictModal + OtherUsersDraftsModal — wrapped in the same guard chain and {#key path} remount. Lift the markup into one component; routes thread their itemKind, path, editPathFor, and loader callback. Pure markup extraction, no state ownership change. Drops the unused userStore import where the trailer was the only consumer. * refactor(drafts): UserDraft.useReactive — kill array-of-one boilerplate The script + flow routes both wanted a handle that re-keys when the URL path changes. UserDraft.use() can't do that (its opts getter is untracked), so each route hand-rolled the same useMany-array-of-one + proxy idiom: const handles = useMany(() => [{ kind, path: reactive }]) const handle = { get draft() { return handles[0]?.draft }, ... } Add UserDraft.useReactive(getSpec) that internally wraps useMany with a single spec and returns the stable proxy. Callers collapse to one line. * refactor(drafts): unify bootstrap suspension via armRestartOnFirstInteraction The flow and raw-app routes each rolled their own end-of-bootstrap resume: a 700ms setTimeout for flows and a templatePicker watcher with double-tick gating for raw-apps. Both are timing-fragile (the comments admit it) and drift from each other. armRestartOnFirstInteraction already existed in userDraftToast.ts for reset-to-deployed: keydown/input/pointerdown listeners (capture phase) that fire restartSync on the first real user touch, with a 5s belt-and-braces fallback. Export it and use it everywhere we'd previously have picked a magic number. For raw-apps this is a tiny behavioural change: the user's template choice now POSTs immediately (the pointerdown that picks the template also resumes sync, so the picker's onStart write rides the wake-up). Previously the choice only persisted on the user's NEXT edit. That's strictly better — navigating away preserves the choice now. * refactor(drafts): type App.draft_path; drop the as-any cast The audit asked for the three editors to converge on one draft_path injection pattern. For App and Flow, the in-builder $effect-mutates- the-store idiom is wedged into a shape that doesn't natively own the field — App's editor type genuinely has no draft_path so the writer had to cast through `as any`, and consumers downstream did the same. The minimum viable fix: declare draft_path on the local App type (it's already a field on the autosaved JSON). Lifting the writes upward into a route-side merger would mean restructuring the AppEditor mirror $effect and the FlowBuilder pathStore plumbing — larger change for the same shape, deferred to a follow-up. Flow already has the typed cast localised at one site. Will get the OpenAPI-level draft_path field as part of task 47 (drop as-any casts on backend overlay reads). * refactor(drafts): extract makeDraftAddLoad helper Four identical /add/+page.ts files differing only by the edit-route prefix. Lift the redirect into a factory, slim each entry point to two lines. * refactor(drafts): type UserDraftOverlay.other_drafts_users in the OpenAPI The backend response carried other_drafts_users on every get-by-path that supports the draft overlay, but the OpenAPI schema didn't declare the field. Each route had to cast the typed response to `any` to read it (and the sibling draft_saved_at), which obscured the real shape from the type system and rotted the discoverability of the draft surface. Add it to UserDraftOverlay. Frontend casts collapse to plain property reads in the three editor routes. * feat(drafts): list & open draft-only items for variables, resources, schedules, triggers For scripts/flows/apps the list and get-by-path endpoints already surface per-user drafts that have no deployed counterpart — that's what gates the home page from 404'ing on an AI-agent-created draft. Extend the same support to the other UserDraftItemKinds: Backend (list endpoints): - Add include_draft_only to ListVariableQuery, ListResourceQuery, ListScheduleQuery, StandardTriggerQuery (the latter covers the 11 trigger kinds via the generic TriggerCrud). - Append per-user draft rows whose path has no deployed row. Same gate as scripts/flows/apps: non-operators, page 0, no narrowing filters. Synthesis is per-kind: ListableVariable/Resource get field-for-field synthesis; ScheduleLight reads NewSchedule shape; Trigger<T> uses a best-effort JSON merge + serde_json::from_value (rows skipped on deserialize failure rather than failing the list). - Add draft_only: Option<bool> with sqlx(default) to each row type so it serializes as the column is opt-in. Backend (get-by-path endpoints): - get_variable, get_resource, get_schedule, get_trigger<T> fall back to fetch_draft_only when the deployed row is missing and the caller passed get_draft=true. Mirrors scripts/flows/apps. OpenAPI: - Shared IncludeDraftOnly parameter under components/parameters, wired into the 11 trigger list endpoints + listRawApps. Inline declarations on listVariable / listResource / listSchedules / listAzureTriggers. - draft_only field on ListableVariable, ListableResource, Schedule, TriggerExtraProperty. Frontend: - variables, resources, schedules, and the 10 trigger list pages (routes + 9 *_triggers) pass includeDraftOnly: true on the initial fetch and render <DraftBadge draft_only> on synthesized rows. Trigger pages got a sed/perl bulk update — pattern is the same across kinds. * fix(drafts): swap crypto.randomUUID() for the project's randomUUID helper crypto.randomUUID() is gated on a secure origin (HTTPS or localhost). Self-hosted Windmill instances often run on a bare HTTP origin or a LAN IP where the WebCrypto API is unavailable, so the /add redirect would throw before issuing the 307. Use the existing RFC4122 v4 helper in FlowChatManager that the rest of the codebase already imports for this exact reason. * fix(editor): leading-edge fire + max-wait cap on Monaco debounce The Editor debounced `onDidChangeModelContent` purely on the trailing edge — every keystroke rescheduled a 500ms timer, and uninterrupted typing held the bindable `code` prop stale until a pause. Stacked behind our 1.5s autosave debouncer that meant our clock didn't even start ticking until 500ms after the user paused, and the `code` binding never updated mid-burst for downstream consumers (lint, live preview, change listeners). Switch to leading + trailing + max-wait: * First keystroke of a burst fires `updateCode` synchronously, then stamps a wall-clock chain start. * Each subsequent keystroke (re)arms a trailing timer at `min(now + changeTimeout, chainStart + maxChangeTimeout)` — the cap is what makes continuous typing materialize at least once per maxChangeTimeout window instead of indefinitely. * When the trailing fires it resets the chain so the next keystroke after a pause is a fresh leading fire. New prop `maxChangeTimeout` (default 1000ms) sits next to the existing `changeTimeout` (default 500ms). Dispose path clears the chain stamp alongside the timer. * feat(drafts): wire Ctrl/Cmd+S to flush the pending autosave immediately Each builder already had a Ctrl/Cmd+S keybinding routed through a saveDraft() no-op left over from the LS-era — the comment said "persistence happens via the page-level UserDraft autosave" but the shortcut was the user's only way to actually force a save without waiting for the 1.5s debounce. Restore the intent. * UserDraftDbSyncer.flush({ workspace, itemKind, path }) — new method that re-submits whatever's queued in pendingSaveOpts with immediate: true. No-op when nothing's pending. * Editor.svelte.flushPendingChanges() — exposes a synchronous updateCode() with chain reset, so callers can drain Monaco's own trailing debounce before asking the syncer to flush. Without this step a Ctrl+S within ~500ms of typing would POST the pre-burst content. * ScriptBuilder.saveDraft() — editor?.flushPendingChanges() → await tick() → UserDraftDbSyncer.flush(). Toast on result. * FlowBuilder.saveDraft() — no direct Monaco ref (flows have many per-module editors); just flushes the syncer. Editor.svelte's new 1s max-wait cap means at most the last <1s of typing in a module Monaco won't be in this POST; it follows in the next autosave round. * RawAppEditor.handleKeydown — adds a 's' case that flushes before the focus guard, so the shortcut fires regardless of where focus is in the editor pane. * fix(drafts): low-code apps — drop spurious autosave on /edit + remount on Load from server Two bugs in low-code app editor (raw apps use a separate code path): 1. Every /edit visit looked like an autosave because loadApp() called UserDraft.discard('app', path, undefined). The comment claimed "this load doesn't POST" but discard always POSTs value: null server-side — that surfaced as a DELETE-my-draft on every page load AND a flash in the AutosaveIndicator. The discard was originally intended to wipe the in-memory cell so AppEditor remounts "fresh". But the path-change $effect upstream already sets app = undefined before each loadApp, which unmounts AppEditor and releases the handle's entry — so a remount via app = backendApp naturally starts with an empty handle. Drop the discard. 2. The conflict modal's "Load from server" called loadApp() but didn't remount AppEditor. Since AppEditor's stateApp is captured once at mount and doesn't react to prop changes, the editor kept showing the conflicting local edits even after a successful reload. Wrap the onLoadFromServer to await loadApp() then bump redraw to force a fresh mount. * feat(drafts): home-page Draft badge — show user-initial circles, drop the '+' The home-page Draft badge previously showed '+Draft' as a flat label. Add per-user awareness: up to 3 user-initial circles render to the left of the label, ordered alphabetically; with 4+ users we collapse to the first 2 + a '+N' overflow circle so rows stay compact. Backend: * New `DraftUserRef { username: Option<String> }` in windmill-types::user_drafts, re-exported from windmill-common so the list endpoints in scripts/flows/apps crates share one import path (windmill-types/windmill-common can't be reordered without a cycle). * ListableScript / ListableFlow / ListableApp gain a `draft_users: Option<sqlx::types::Json<Vec<DraftUserRef>>>` field. The list SQL adds a per-row subquery `SELECT json_agg(...) FROM draft d LEFT JOIN usr u ...` that aggregates the workspace users with a per-user draft at this path. NULL (no drafts) decodes to None; LEFT JOIN against `usr` lets orphaned drafts (user removed from workspace) still surface with username = None. * Synthesized draft-only rows set draft_users to a single-element vector with the authed user (those rows come from `email = $2`). OpenAPI: `draft_users` added to listScripts / listFlows / ListableApp response shapes as an array of `{ username }` with nullable username. Frontend DraftBadge: * Accepts `draft_users: { username?: string | null }[]`. Renders up to MAX_CIRCLES (3) initial circles; at 4+ users renders first 2 + a gray '+N' overflow circle. * Initials: 'john.doe'/'john_doe' → 'JD', 'alice' → 'AL', the legacy NULL-email row → '?'. * Color picked deterministically from a 6-entry palette so the same user gets the same circle color across rows. * Label is now just 'Draft' (dropped the '+'). 'Draft only' is unchanged. * Tooltip lists every user in full. ScriptRow / FlowRow / AppRow thread `draft_users` through their prop types and pass it to DraftBadge. * fix(drafts): suppress 'You have unsaved changes' banner when deployed baseline is null A brand-new variable/resource/trigger (no deployed row yet) has `getDeployed() == null`, but the caller's `show` prop is computed off `current != deployed` which is trivially true while the user types. Result: the banner appeared with 'Show diff' (no-op — the drawer early-returns on null deployed) and a 'Discard' that's semantically backwards (there's nothing to revert to). Gate `show` internally on `getDeployed() != null`. The check sits in the banner rather than each caller because every caller would otherwise need the same boilerplate guard. * fix(drafts): hide LocalDraftBanner when deployed and current match the DiffDrawer's compare Earlier I gated the banner on `getDeployed() != null`, but the user still saw it fire on entries where 'Show diff' opens to 'No changes detected'. That means `show` (the caller's coarse dirty check) flagged a difference the DiffDrawer treats as a no-op — typically toggle defaults (`false ↔ undefined`), removed empty arrays, or key-ordering noise that `cleanValueProperties + orderedYamlStringify` collapses. Replicate the drawer's comparison inside the banner: stringify both sides through the same pipeline and only render when the keys differ. A single `diffKey()` helper keeps the logic local; the catch-and-empty fallback survives a non-serializable side rather than throwing. * ui(drafts): nest user-initial circles inside the Draft badge Previously the circles sat alongside the Badge in a parent flex container; the result read as two separate UI elements. The Badge component already exposes its children as a snippet rendered inside its own flex row, so moving the circles into it makes them feel like part of the same chip. Knock-on tweaks: shrunk the circles from h-4/w-4 to h-3.5/w-3.5 so the badge stays compact, and tinted each circle's ring with the badge's indigo palette (instead of plain white) so the overlap reads as a deliberate stack rather than dots floating on top of the chip. * feat(drafts): drop the authed user's circle, mark own drafts with a '*' suffix Three tweaks to the home-page Draft badge: 1. Filter the authed user out of `draft_users` before rendering circles. The row already signals 'this user has a draft' via the asterisk (below), so a circle for them would be redundant noise. New `currentUsername` prop on DraftBadge — pass `$userStore?.username` from each row. The tooltip still lists every user (with `(you)` next to the authed one) so the full picture is one hover away. 2. The badge already showed whenever `is_draft || draft_users.length > 0` (per-user OR any-user). Spelled the rationale out in a comment — no logic change. 3. Append '*' to the displayed summary when `is_draft` is true. Falls back to `draft_path`/`path` when summary is empty so the marker never decorates an empty string. Threaded the same expression into ScriptRow / FlowRow / AppRow. Slice/overflow math now keys on the post-filter `otherUsers` list, so dropping the authed user doesn't silently shrink the visible count (e.g. 3 users incl. self → 2 circles, not 1 circle + a '+1' bubble). * feat(drafts): clone per-user drafts when forking a workspace `clone_workspace_data` clones every other workspace-scoped table on fork creation (resources, variables, scripts, flows, apps, raw apps, triggers, schedules) but quietly dropped the `draft` table. With per-user drafts that meant any open editor in the parent lost its pending edits the moment a fork was created — surprising and inconsistent with how forks treat the deployed surface. New `clone_drafts` mirrors the existing clone helpers: a single INSERT...SELECT into the target workspace, preserving `path`, `typ`, `value`, `created_at`, and `email`. The `email` FK targets `password.email` which is instance-scoped so it carries across workspaces without remap. `created_at` is preserved on purpose so the per-tab `last_sync` baseline lines up with the parent's timeline — otherwise the fork's next autosave would race a stale `last_sync` and trip the conflict modal on every cloned draft. Plain INSERT (not UPSERT) is safe because the fork target is empty at create time; no conflict against the partial unique indexes (`draft_pkey_with_user` / `draft_pkey_legacy`). The synthetic BIGSERIAL `id` PK is regenerated by the default so it stays out of the column list. * ui(drafts): pin the authed user to the first circle instead of hiding them Previously the authed user was filtered out of the circle row entirely on the theory that the row's '*' suffix already signalled 'this user has a draft'. New requirement: they should always lead the circle row when they have a draft so the visual half of the signal lines up across rows (consistent leading-slot identity, easy scan). Switch from a filter to a sort: `orderedUsers` finds the authed user in `draft_users` and splices them to index 0; everyone else keeps the backend's alphabetical order behind. Slice/overflow math now keys on `orderedUsers`, which guarantees the authed user never falls into the '+N' bubble — they're at position 0 and the slice keeps the head. The popover's '(you)' annotation moves to the circle's title attr too, so hovering the leading circle confirms the identity. * feat(drafts): drop draft_only column from script/flow/app Drafts now live in the `draft` table exclusively — `draft_only` stubs in script/flow/app are redundant. Migration `INSERT INTO draft ... ON CONFLICT (workspace_id, path, typ) WHERE email IS NULL DO NOTHING` so real per-user drafts already at the same path are preserved; only rare stubs that lost their draft get a synthesised workspace-level row. Stubs are then deleted (FKs cascade to *_version) and the column is dropped. List endpoints keep a synthesised `draft_only: true` on rows sourced from the draft table itself (sqlx default on the struct field). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ui(drafts): surface draft state in AutosaveIndicator instead of toast+auto-modal The "Loaded your saved draft" toast and the auto-opening OtherUsersDraftsModal both surprised users on every editor mount. Move both signals into the AutosaveIndicator label: "Loaded from draft" or "Others are working on this {kind}" (priority) sits where Saving/Saved do, with a one-shot light-green flash behind the indicator that fades to transparent. Saving/Saved still win when they fire. The popover gains a "See others' drafts" button that flips the modal open on demand; the modal itself is now externally controlled via a bindable \`isOpen\` threaded through DraftEditorModals. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ui(drafts): per-user View JSON / Fork actions in DraftBadge popover Hover popover used to be a plain text list of usernames. Now each row gets a colored circle icon + name + "(you)" for the authed user, and every OTHER user's row carries View JSON / Fork buttons mirroring the OtherUsersDraftsModal. For draft-only entries owned solely by the authed user, the popover ends with "Only you can see this {kind}" so the row's privacy is obvious. ScriptRow / FlowRow / AppRow thread workspace + itemKind + path + editPathFor through; AppRow switches between app / raw_app on app.raw_app. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * nit * fix(drafts): clone only the forker's per-user drafts on workspace fork clone_drafts copied every user's drafts, but only the forker gets added to the fork's usr table. Drafts owned by absent users LEFT-JOIN to NULL in the home page's draft_users aggregate, surfacing as multiple legacy-style rows at one path and crashing the popover with each_key_duplicate. Filter the clone to email = forker OR email IS NULL, and key the popover's #each by index defensively so future legacy collisions can't crash the page either. Also re-adds `draft_only: None` to NewScript/CreateFlowBody literals in tests — the auto-generated windmill-api-client still carries the field and the previous commit dropped them too aggressively. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(drafts): always populate other_drafts_users in maybe_overlay_draft Reset-to-deployed reloads the deployed payload with get_draft=false, which made the backend return other_drafts_users=[]. The route then reassigned otherDraftsUsers to the empty list, dropping the count to 0 and hiding "See others' drafts" in the AutosaveIndicator popover — but the other users' drafts hadn't actually gone anywhere. Fetch the list independently of get_draft so the popover stays accurate across reset reloads. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(drafts): alert user when their draft is older than the latest deploy Open a modal on editor mount when the per-user draft was saved before the latest deploy at the same path — i.e. a teammate deployed a new version while this user's draft was sitting. Two choices: discard the stale draft and pick up the deploy, or keep editing the older draft. DraftEditorModals computes the staleness from the timestamps each route threads in (script.created_at, flow.edited_at, app_version.created_at) and the "Load latest deploy" callback reuses the route's existing reset-to-deployed logic. Wired for script / flow / app / raw_app editors; trigger / resource / variable drawer editors follow a different pattern and aren't covered here. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(drafts): deploy only wipes the deployer's draft, not everyone else's Script / flow / app deploys ran an unconditional DELETE on every draft at the path, so a teammate's deploy silently destroyed any other user's pending draft. After the wipe, the other user's tab kept auto-saving — re-creating the row at a NOW timestamp newer than the deploy — and StaleDraftModal never fired because draft_saved_at had been bumped past the deploy. Filter the DELETE to email = deployer (plus the legacy NULL row), so other users' drafts persist and the stale-draft prompt actually fires on their next reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(drafts): surface save failures in AutosaveIndicator instead of pretending Saved postSave caught network errors with `console.error` and let the runner finish normally. The indicator read the saving → none transition as a successful save and flashed "Saved" even when the request had thrown. Track failed keys in a SvelteMap, expose `'failed'` as a new UserDraftSyncState, render "Save failed" in red with a CloudOff icon. Failure clears on the next successful save for the same key, or when recordRemoteSync seeds a fresh authoritative timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(drafts): surface 'Save failed' inside the AutosaveIndicator popover too The popover used to repeat the cheerful "All changes are saved as a draft on the server..." copy even when the inline label said "Save failed", which read as contradictory. Add a red, text-xs warning at the top of the popover body when the sync state is `failed`, explaining that the latest edits didn't reach the server and that editing again retries the save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(drafts): surface the actual error message in the AutosaveIndicator popover Replace the generic "your latest changes did not reach the server" copy with the real failure detail. The syncer now stores the extracted message in the failures map (formatSaveError walks body / message / statusText) and exposes it via the state handle's `failureMessage` getter. Popover renders it in red, monospaced, scrollable so a long server traceback doesn't blow out the popover. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(drafts): suppress Saving/Saved indicator during a reset-to-deployed discard A `value: null` POST is a discard, not a save, but it ran through the same runner the indicator watched — so resetting to deployed flashed "Saving..." → "Saved", reading as "your draft just landed" while we were actually wiping it. Track in-flight discards in a SvelteSet, expose a distinct `'discarding'` UserDraftSyncState, and the indicator stays quiet for it: no spinner, no label change, and the `discarding → none` transition deliberately skips the "Saved" flash. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Revert "fix(drafts): suppress Saving/Saved indicator during a reset-to-deployed discard" This reverts commit |
||
|
|
392888d113 |
docs: add SAFETY comments to all dynamic SQL call sites (#9009)
* docs: add SAFETY comments to all dynamic SQL call sites Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs: address review feedback on SAFETY comments - Fix missed comment for obo_triggers loop in offboarding.rs - Fix variable name in comment (table -> table_name) in offboarding.rs - Fix api-settings comment to reference inline VALID_NAME regex, not validate_dbname() - Add SAFETY comments to batch_execute calls in api-settings - Fix db.rs comment: PG_SCHEMA is env var, not compile-time constant - Add doc comments on RunnableSettingsTraitInternal constants * docs: remove misleading SAFETY comment on static SQL --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> |
||
|
|
d6c642b170 |
feat: add Azure Event Grid triggers (#8888)
* feat: add Azure Event Grid triggers (EE)
Introduces a new enterprise trigger kind `azure` that supports three
modes via a single unified trigger type:
- basic_push: Azure Event Grid basic — custom topics, system topics
(Storage, Resource Manager, Key Vault, etc.), domains (push only)
- namespace_push: Event Grid Namespace topics (CloudEvents over HTTP push)
- namespace_pull: Event Grid Namespace topics (HTTP pull with lock-token
ack/reject for dead-lettering)
Auth uses a Service Principal resource (tenant_id, client_id,
client_secret, subscription_id). Subscriptions are created in
CloudEvents 1.0 schema so the push webhook handler and the pull listener
share one payload parser.
Backend
- New crate `windmill-trigger-azure` (OSS stubs + EE impl symlinked from
windmill-ee-private)
- Migration `azure_trigger` table with CHECK constraints enforcing
mode/columns coherence
- `TriggerKind::Azure`, `JobTriggerKind::Azure`,
`DeployedObject::AzureTrigger` variants
- Push route `/api/azure/w/{workspace}/*path` handles classic
Event Grid SubscriptionValidation handshake and CloudEvents 1.0
abuse-protection OPTIONS handshake
- Optional inbound JWT validation (audience check only for v1)
- Feature flag `azure_trigger` propagated through windmill-api,
windmill-store (resource helper), and added to ee_core
Frontend
- `triggers/azure/` editor with mode toggle (basic/namespace-push/
namespace-pull) and per-mode config (topic ARM id / namespace +
topic name / subscription / filters / push auth / pull options)
- Registered in icon map, display names, save functions, badge,
wrapper, editor, add-trigger menu
OpenAPI
- `AzureTrigger`, `AzureTriggerData`, `AzureMode`,
`AzureSubscriptionMode`, `AzureDeliveryConfig`, `TestAzureConnection`
schemas; `/azure_triggers/*` endpoints; client regenerated
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to eaa7c3a9cb37a9ccc93f10a2535d929365acd2d8
This commit updates the EE repository reference after PR #541 was merged in windmill-ee-private.
Previous ee-repo-ref: 9689014e8c12c36c1059fd8fa5758d550b8b8bc9
New ee-repo-ref: eaa7c3a9cb37a9ccc93f10a2535d929365acd2d8
Automated by sync-ee-ref workflow.
* feat(azure-trigger): secret-auth push, ARM discovery, capture isolation, CLI + parity
Frontend:
- Split mode selector into Namespace/Basic + Pull/Push
- ARM resource dropdowns (namespaces, Basic topics, namespace topics)
populated from the service principal; cascade with stale-selection
reset on SP / edition change
- Remove stale authenticate toggle + audience input (server-managed
push_auth_config has replaced them)
- Azure listing page: "Create from template" button; "Also delete Azure
subscription" toggle in the delete modal; simplified trigger label
falling back to path
- AzureCapture.svelte: "Test subscription name" with -wm-capture suffix
- CompareWorkspaces.svelte: wire Azure for fork/compare
- Drop Trigger-deployed/event-loss warning (capture subscription is
isolated with -wm-capture)
Backend:
- Shared-secret push auth (see EE crate for detail)
- JSONB push_auth_config column (renamed from delivery_config), #[serde(skip)]
so clients/CLI/exports never see it
- Drop redundant enabled column; mode supersedes
- Azure capture infra: AzureTriggerConfig + set_azure_trigger_config +
azure_payload route + TriggerKind::Azure arm; PT15M queue TTL on
capture subscriptions so they bound storage after tab close
- Granular ACLs, users offboarding, trash, git-sync deployed-object:
all include azure_trigger
CLI:
- Add azure to TRIGGER_TYPES, pushObj dispatch, getTypeStrFromPath,
trigger commands (get/update/create/list/template), sync delete
switch + regex; e2e test for `trigger new --kind azure`
- system_prompts: SCHEMA_MAPPINGS + schema_names include AzureTrigger;
auto-generated/* regenerated
Skill:
- .claude/skills/adding-a-trigger/ checklist covering every file that
needs editing when wiring a new trigger type (learned from this PR)
ee-repo-ref bumped to b0e490cbf3724b7b64c6a5b010e3bdf24acd873c.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(azure-trigger): ci — ShareModal Kind + regenerated system_prompts
- frontend/src/lib/components/ShareModal.svelte: add 'azure_trigger'
to the Kind type so the listing page's "Permissions" action compiles
(ts2345 — caught by npm_check on CI, missed by fast-check locally).
- system_prompts/auto-generated/: regenerate to drop the stale
delivery_config / AzureDeliveryConfig fields from the Azure schema
(check-freshness on CI).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(azure-trigger): use workspace constant_time_eq crate
Drop hand-rolled constant-time compare in favour of the workspace
constant_time_eq crate (same one used by http_trigger_auth).
ee-repo-ref bumped to 9659382d47286e7f7f66d01b6f5dd8d4ed34848b.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(azure-trigger): pass placeholder + disabled via inputProps
`TextInput`'s `placeholder` and `disabled` go through its `inputProps`
prop — CI's `npm run check` caught the stale top-level passing that
`npm run check:fast` missed. Align with the DefaultEmailConfigSection
pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(azure-trigger): correct LATEST_GIT_SYNC_SCRIPT_PATH version to 28213
The hub deploy of the azure-aware sync-script is version 28213, not
28214. Backend was pinning a non-existent hub script, which broke the
git_sync_e2e suite (every deploy's sync step 404'd).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(azure-trigger): add azure_triggers to token scope selector + skill
- windmill-api/src/token.rs: `build_trigger_scope_domains` was missing
`("azure_triggers", "Azure Event Grid")`, so the CreateToken UI's scope
selector didn't surface azure_triggers:read/write. Backend already had
`ScopeDomain::AzureTriggers` wired (scopes.rs), this just exposes it.
- .claude/skills/adding-a-trigger/SKILL.md: capture both scope-related
files under the hardcoded-arrays section so future triggers don't miss
the UI surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(adding-a-trigger-skill): clarify token.rs scope effect
Not a regression — nothing was working before. Skipping TRIGGER_DOMAINS
just means the scope works via API/CLI but has no UI checkbox.
* docs(adding-a-trigger-skill): trim token.rs bullet
* fix(azure-trigger): regen openapi-deref + swap textarea for TextInput
- Run build_openapi.sh to regenerate openapi-deref.{yaml,json} with the
12 azure_triggers paths + schemas. These files are served by the
runtime (include_str! in windmill-api/src/lib.rs) to external SDK
consumers; without this regen the new endpoints wouldn't be advertised.
- Replace the raw <textarea> for event type filters with the
design-system TextInput in textarea mode (frontend/CLAUDE.md bans raw
HTML elements).
Addresses cubic + claude PR review items.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
|
||
|
|
d99a176b6a |
fix: update on_behalf_of_email in app policy on offboarding (#8858)
* fix: update on_behalf_of_email in app policy on offboarding Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * sqlx --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
435b25e6a4 |
feat: add user offboarding flow with object reassignment (#8647)
* feat: add user offboarding flow with object reassignment Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: require new_operator for permissioned_as when reassigning to folder Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update on_behalf_of_email on scripts/flows during offboarding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: extract offboarding to separate module and add integration tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: delete tokens, add operator preview counts, remove token reassignment UI Tokens are now always deleted during offboarding. Preview now shows scripts/flows/apps with on_behalf_of and schedules/triggers with permissioned_as referencing the departing user (even outside their path). Token reassignment UI removed since webhooks break on path changes anyway. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: rich preview with path lists, warnings, and downloadable report Preview now returns full path lists (not just counts) for owned objects and objects executing on behalf of the user. Adds warnings for: - HTTP triggers (webhook URLs will change) - Email triggers (addresses will change) - Broken $var:/$res: references in resources/variables Frontend provides "Export list" button to download affected content. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add coverage for dynamic queries (triggers, extra_perms, operator schedules) Adds HTTP trigger, extra_perms reference, and shared schedule to test fixture. Tests verify that non-macro sqlx queries (trigger reassignment, extra_perms cleanup, operator schedule update) work correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove broken_references, add full dynamic query test coverage Remove broken_references field from preview (user's resources/variables are already in the owned paths list). Add shared HTTP trigger fixture to test all dynamic query paths: trigger operator preview (line 232), trigger permissioned_as update for non-user-path (line 951), and extra_perms cleanup on trigger tables (line 983). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add referencing field to preview for content/value path references Preview now includes a 'referencing' section listing scripts (by content), flows (by value JSON), apps (by policy/extra_perms), and resources (by value) that contain references to u/{username}/ paths. These references may break after reassignment. Shown in export list and as a warning in the UI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: rename objects to items in UI, detect on_behalf_of items in hasItems Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: replace remaining objects with items in UI text Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: rename operator to on_behalf_of, separate owned vs on-behalf UI sections - Rename new_operator to new_on_behalf_of_user in API and frontend - Rename op_ prefixed variables to obo_ in backend - UI now shows separate sections for owned items and items running on behalf, with the operator selector shown only when needed - canSubmit logic updated: operator needed for folder targets OR when on-behalf items exist Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: CSV export, side-by-side layout, always accept new_on_behalf_of_user - Export affected items as CSV instead of text - Owned items and on-behalf items shown side by side in summary boxes - new_on_behalf_of_user always accepted (defaults to target user for user targets, required for folder targets) - On_behalf_of selector always visible, auto-defaults when user target is selected Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: proper pluralization and bottom-aligned counts in summary boxes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: stack info boxes vertically, referencing box as warning style at top Info boxes (owned, on-behalf, referencing) now one per row instead of side-by-side. Referencing box uses warning colors. Webhook/email trigger alerts shown below boxes. Proper pluralization in global modal too. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: CSV exports only referencing items, export button inside warning box Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: use ToggleButtonGroup for user/folder, add reassign toggle on remove - User/Folder selection now uses ToggleButtonGroup component - When removing a user, a "Reassign items before removing" toggle lets the admin skip reassignment and just delete directly - In reassign-only mode, the toggle is not shown (always reassigns) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: show token details with labels and scopes in preview Preview now returns token label, scopes, and expiration instead of just a count. Frontend shows a dedicated token box listing each token with its scopes. Test updated to verify token label in preview response. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: extract shared offboarding components, per-type trigger links, hash deep linking - Extract OffboardItemsBox, OffboardReassignControls, OffboardWorkspaceSection, and offboarding-utils.ts as shared components used by both workspace and global modals - Change triggers in OffboardAffectedPaths from Vec<String> to HashMap<String, Vec<String>> so frontend knows which trigger page to link to - Add hash-based deep linking to all 9 trigger pages and schedules page - Preserve URL hash in updateQueryFilters across all trigger pages - Only open editor drawer if the item is found in the list - Reassign toggle at top with warning alert when disabled (both modals) - Referencing items box uses yellow warning variant with expandable path links - Cleaner labels: "Move u/{username}/* items to", "Update triggers/runnables permissions to" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: rename on_behalf_of section label to match flow advanced settings Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: regenerate sqlx query cache Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review issues for offboarding - Add 9 trigger tables to check_path_conflicts for user-friendly conflict messages - Fix submit button no-op when user has only on-behalf items (show target selector, fix canSubmit) - Only delete workspace user when reassignment entry exists (prevent orphaned objects) - Add $azure_kv: prefix to vault secret query (match rename_user pattern) - Use Svelte 5 onSelected callback instead of deprecated on:selected - Make ScriptBuilder section label conditional on canPreserve - Fix CSV export to include trigger paths via flattenPaths utility - Fix test_offboard_reassign_only to remove conflicts and assert on response - Parallelize workspace config fetches in global modal with Promise.all - Delete tokens when deleting workspace user - Return structured JSON from global offboard endpoint Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * sqlx * fix: address second round of PR review issues - Accumulate per-workspace OffboardSummary in global offboard instead of returning zeros - Delete workspace user unconditionally when delete_user=true (prevent orphaned usr rows) - Filter archived/deleted scripts in check_path_conflicts to match preview - Reset form state when workspace offboard modal reopens - Move hashHandled=true inside trigger-found guard on all 10 deep-link pages Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: improve offboarding integration tests - Add second workspace to fixture for multi-workspace global offboard testing - Add test_global_offboard_execution: verifies items reassigned across 2 workspaces, user deleted from both, and password row deleted from instance - Add test_offboard_invalid_target: verifies 400 for nonexistent user, nonexistent folder, and invalid target format - Fix test_offboard_to_user: use single DELETE, add explicit new_on_behalf_of_user - Fix test_global_offboard_preview: assert 2 workspaces instead of 1 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address third round of PR review issues - Fix ScriptBuilder tooltip to match conditional section label wording - Clear stale conflicts in global modal on reopen - Fix test_offboard_to_folder to assert on specific moved path, not pre-existing data - Allow deleting user with zero items (show Offboard button, skip reassignment) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add global token deletion warning in instance-level offboard modal Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * update sqlx * fix: add raw_app path and dependency_map path reassignment to offboarding Audit found these tables with user-scoped paths were not being updated: - raw_app: mirrors app paths, needs path reassignment - dependency_map: importer_path and imported_path reference user paths Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: move user cleanup to delete_workspace_user_internal, fix review issues - Move extra_perms, folder owners, drafts, favorites, inputs, captures cleanup into delete_workspace_user_internal so any user deletion gets proper cleanup (not just offboard path) - Fix flow INSERT missing labels and lock_error_logs columns (data loss) - Fix validate_target returning 404 instead of 400 for nonexistent targets - Fix canSubmit blocking delete when user has no items to reassign - Fix token preview query filtering out tokens without scopes - Fix token warning messages: workspace-level mentions webhooks/HTTP triggers, instance-level mentions API calls using credentials - Fix "Schedules and triggers" -> "Triggers and runnables" wording - Show token section at instance level only when tokens exist - Show Offboard button at instance level when user has no items but deleteUser=true Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |