`states[ws].draft` is now a Svelte $state proxy (it flows through
UserDraft's useLocalStorageValue cell). `structuredClone` can't clone a
proxy and threw "Failed to execute 'structuredClone' on 'Window'",
blocking resource saves. Snapshot to a plain object via
`$state.snapshot` before assigning the dirty baseline.
The "Reset to deployed" toast action in resource/variable editors
called UserDraft.save with the backend value to repaint the form. That
left a duplicate-of-backend autosave in localStorage which would
silently restore on every subsequent reload, defeating the reset.
New UserDraft.discard(itemKind, path, fallback) clears LS AND resets
any live handle's in-memory state to the fallback, skipping the next
persist so the fallback doesn't round-trip back into storage. Backed
by a new `skipNextWriteOnce()` method on useLocalStorageValue's return.
Resource and variable editors silently loaded LS autosaves over the
backend value, leaving users with no signal that the form wasn't
reflecting deployed state. Both now fire the standard
`notifyRestoredFromLocal` toast (with a "Reset to deployed" action
that re-seeds the handle from the just-fetched backend) the first
time a lazy-fetch finds the local draft diverging from the remote.
Two related issues in the app editor's UserDraft wiring:
1. Drift wasn't detected on first deploy/draft after starting an
autosave. The route only backfilled meta on a reload that found a
local diff — so the first external change after editing slipped
through with empty `previousMeta`. AppEditor now receives the
load-time revs as `initialRevs` and seeds them into the handle's
meta on the first mirror, capturing the rev at autosave-creation
time.
2. /apps/add didn't restore from LS on plain reload. The route
always initialised `value` to `emptyApp()` and the AppEditor's
`stateApp` captured the prop unconditionally, so the LS autosave
was shadowed. `stateApp` now falls back to `appDraftHandle.draft`
when present; the template/hub/import branches explicitly
`UserDraft.remove('app', '')` to keep "start fresh from this
content" semantics.
Also work around `useLocalStorageValue`'s `saveInitialValue: false`
skip slot — in the mirror pattern the slot survived past mount and
swallowed the user's first edit. Consume it up-front with a
wipe-then-restore pair so subsequent edits persist normally.
Public surface change:
- New `UserDraft.useMany(getSpecs: () => UserDraftSpec<V>[])` returns a
reactive array of handles. The reconcile loop acquires entries for
added specs, releases entries for removed specs, and re-uses cached
handles for unchanged keys so caller-captured references stay stable.
- `UserDraft.use(kind, path, opts?)` becomes a 1-len wrapper around
`useMany`. The spec getter is `untrack`ed so reactive opts
(`$workspaceStore` etc.) are still captured-once — current `use()`
semantics unchanged.
- `UserDraftHandle.release()` and the `manualRelease` option are gone.
Component teardown is handled by a single internal `onDestroy` that
releases every entry `useMany` acquired.
ResourceEditor + VariableEditor migrated:
- Replaced `Record<ws, Handle>` + manual `ensureHandle`/`release` with
a `workspaceSpecs: $state<Array<{ws, defaultValue}>>` plus a
derived `Record<ws, Handle>` that pairs each ws with its parallel
handle from `useMany`. `ensureHandle(ws)` is now just a push to
the specs array; `VariableEditor.reset()` clears it. The reconcile
loop handles acquisition/release end-to-end.
Tests:
- Dropped the `manualRelease`/`release` test; the option no longer
exists.
- Added a `useMany` test asserting per-spec entries, isolated
workspace-scoped localStorage keys, and a single onDestroy
registration covering every acquired entry.
Implementation note: I tried wrapping `useLocalStorageValue` in
`$effect.root` to give the entry's `$state`/`$effect` an independent
scope (in case `useMany`'s reconcile effect tore down nested effects
across cycles). But `$effect.root`'s callback wasn't running
synchronously in the test runtime (vitest + svelte-vite plugin), and
the original `use()` implementation called `useLocalStorageValue`
directly without issue. Reverted to the direct call; the
nested-scope concern stays theoretical.
Three low-priority callouts:
- Document the variable.edited_at backfill in the migration. All
existing rows get a single `now()` timestamp from the column
DEFAULT; the staleness check only consumes the field as an opaque
rev string and never displays/sorts on it, so the collision is
harmless — but worth saying out loud.
- Add module headers to userDraftToast.ts and LocalDraftStaleModal.svelte
explaining how this layer sits above the per-browser UserDraft
autosave and is distinct from the backend DraftService (the
server-side "Save as draft" feature surfaced as `*.draft`).
None of the eight `new Triggers(...)` call sites pass an update
callback any more — the bridge was a leftover from the pre-UserDraft
era when ScriptBuilder ran its own localStorage autosave and had to
be notified on every triggers mutation. The unified UserDraft handle
now deep-tracks `script.draft_triggers` via the $effect in
ScriptBuilder, so the callback channel is dead weight.
Removes the third constructor parameter, the private field, and the
six `this.#updateDraftCallback?.()` invocations across setters and
mutators.
The four route-level editors (scripts/flows/apps/apps_raw) keep the
user's local draft visible behind the modal so they can glance at it
before choosing. The old body text described the situation (server
has moved on, local autosave is behind) but didn't say what's
actually on screen or how each action maps to it.
New body leads with "The editor is showing your local autosave" and
spells out each action: "Load latest replaces what's on screen; Keep
current leaves it alone." Same copy for both `cause = 'draft'` and
`cause = 'version'`, branching only on what the user is "behind"
relative to.
The triggers in ScriptBuilder live in a dedicated `triggersState`
$state, separate from the `script` object that the UserDraft handle
deep-tracks. Pre-PR the per-builder localStorage autosave bridged the
two by snapshotting `triggersState.getDraftTriggersSnapshot()` into
the payload on every write — that bridge was dropped when we removed
the per-builder autosave in favour of UserDraft.
Add an $effect that deep-reads triggersState and mirrors the snapshot
back into `script.draft_triggers`. The UserDraft handle (already
deep-tracking `script`) then persists the trigger drafts as part of
the script autosave, restoring the prior behaviour.
* feat: include service accounts in instance settings users list
Service accounts (workspace-scoped, no password row) now appear in the
superadmin users list with a Bot icon, workspace badge, and a link to
manage them in the workspace settings. Role is locked to Operator.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: update sqlx offline cache
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: use composite key for users each block
Service accounts can share emails across workspaces, so key by email + workspace_id to avoid Svelte each_key_duplicate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empty-path UserDraft entries persist now. Drop the leftover "in-memory
only" comments on the /add pages' handle creation, and rewrite the
EditorHeader save-initial-draft comments to describe why the UserDraft.remove
call is still needed: the draft was promoted to a real path on the
backend, so the prior-path autosave must not shadow a future "+ App" /
"+ Flow" / … visit.
Empty paths used to be in-memory only (via the `isLocalOnly` short-circuit)
because we worried about collisions between concurrent /add tabs. The user
asked for the trade-off to flip: a /flows/add or /scripts/add reload should
restore the user's work, while explicitly clicking "+ Flow / + Script / …"
should always open a clean editor.
- Drop `isLocalOnly` from UserDraft so empty-path entries persist under
`userdraft/w/{ws}/{kind}/` like any other path. The existing per-kind
refcounting and saveInitialValue=false behavior already handle them
correctly — the change is just lifting the bypass.
- Each /add page now calls `UserDraft.remove(kind, '')` synchronously
when `?nodraft=true` is present in the URL, before the handle is
created.
- The two "+" entry points that lacked the `?nodraft=true` flag
(CreateActionsScript's plain `<a href>` and CreateActionsFlow's
YAML/JSON import paths) now include it, so every fresh-start path goes
through the wipe.
- Tests updated: the "empty path (in-memory only)" block becomes
"empty path (persists across reloads)" and asserts the new behavior.
* feat: read-only flag on API tokens, orthogonal to scopes
Add a per-token `read_only` boolean set at creation time. When true, the
token can only call HTTP methods classified as Read (GET/HEAD/OPTIONS).
Mutating methods and job-run actions are rejected with 403, regardless of
which scopes are attached. Surfaced as a prominent toggle in the standard
token-creation flow and a discreet `2xs` toggle in MCP mode (where users
often want write access, so we don't bias them toward enabling it).
MCP enforcement: read-only tokens hide all script/flow/hub tools from
`list_tools` and only see endpoint tools whose method is GET, and the
runner rejects `call_tool` on anything mutating.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: review fixes for read-only token flag
- Exempt /api/mcp/* and /mcp/* paths from the read-only middleware check.
MCP transport runs over POST (streamable HTTP / SSE), so otherwise the
middleware would 403 every MCP request before the runner could enforce
read-only at the tool-call level.
- Tighten is_endpoint_read_only to GET only, matching the read_only_hint
that create_endpoint_annotations actually emits.
- Add unit test for check_read_only_for_route covering GET/HEAD/OPTIONS,
mutating methods, and run paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: bump ee-repo-ref to read-only-trigger-toggle
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(frontend): make read-only toggle discreet in both modes
Match the MCP-mode treatment in standard mode: text-tertiary, 2xs, shared
"Read-only" label. The tooltip switches per mode so the explanation still
fits the context.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(frontend): gate read-only toggle behind Limit token permissions
The read-only toggle now only shows when the user has limited the token's
scopes (standard mode) or in MCP mode (which always picks an MCP scope).
Turning the limit off also resets read-only so it doesn't silently stick.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(frontend): hide incompatible MCP tools when read-only is on
When the read-only toggle is on in MCP mode:
- Endpoint badges and the custom-mode endpoint MultiSelect filter to GET.
- Already-selected non-GET endpoints are pruned from the scope.
- The scripts/flows preview is replaced with a note explaining they're
hidden (the runner already rejects script/flow runs for read-only).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(frontend): place read-only toggle at top of limited scope area
The previous gate required at least one scope to be picked before the
read-only toggle appeared, which made it look missing while the user was
still building their scope list. Move the toggle inside ScopesPicker:
- Standard mode: sits directly under the "Limit token permissions" toggle
whenever Limit is on, before the scope selector.
- MCP mode: sits at the top of the MCP scope block.
readOnly is now $bindable on ScopesPicker so CreateToken still owns the
value. The auto-reset on un-limit moves into ScopesPicker too.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(frontend): nest read-only toggle inside the scope list card
Place the read-only toggle at the top of the scope list (between the
Selected Scopes summary and the bordered domain list) via a new optional
topSlot snippet on ScopeSelector. Keeps ScopeSelector decoupled from
read-only specifics; ScopesPicker fills the slot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 9bc8160be50b3e57a60daf4e1b71c389a6e02b8a
This commit updates the EE repository reference after PR #571 was merged in windmill-ee-private.
Previous ee-repo-ref: f53d26e6685dfd60bfa67686fbd7358169cfd130
New ee-repo-ref: 9bc8160be50b3e57a60daf4e1b71c389a6e02b8a
Automated by sync-ee-ref workflow.
* fix: address CI review for read-only token flag
- P1 (Codex): narrow the MCP middleware exemption from "any /api/mcp/*"
to just the streamable HTTP transport endpoints
(/api/mcp/gateway, /api/mcp/w/{ws}/{mcp,sse,list_tools}). Without this,
a read-only token could POST /api/mcp/gateway/oauth/server/approve and
mint a follow-on non-read-only MCP token via the OAuth code/token
exchange.
- P2 (Claude/cubic): fix test comment/assertion mismatch — the run-path
assertion now exercises GET (which is what the RUN_PATH_ACTIONS
elevation comment describes) in addition to POST. Add a regression
assertion for /api/mcp/gateway/oauth/server/approve.
- P2 (cubic): short-circuit script/flow/hub-script/resource fetches in
MCP list_tools when read_only is on — they would only be discarded
below, so skipping the DB and resource fan-out is pure win.
- P2 (cubic): when scopes are pre-supplied via the CreateToken prop, the
ScopesPicker isn't rendered, which previously hid the read-only
toggle entirely. Render it next to the pre-supplied scopes display.
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>
* perf(dynselect): only retrigger when helper-script args actually change
Parse the inline helper's signature with the existing WASM parser and
restrict the form-arg diff to keys the helper actually consumes. Typing
into unrelated fields no longer queues a dynselect job every second.
Falls back to the previous full-args comparison when the helper is
deployed or parsing fails.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(dynselect): avoid double helper-script fetch on mount
usePromise defaults to loadInit=true, so refresh() ran before the
JobLoader child was bound (firing a no-op pending promise) and the
$effect then fired a second refresh once the bind:this resolved.
Disable loadInit so the effect owns the single first call.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(dynselect): use parser directly instead of inferArgs
inferArgs mutates a Schema object we never use and goes through a
shared cache; when fed an empty schema for non-main entrypoints the
caller cannot reliably read back the resulting properties. Add
parseEntrypointArgs that just runs the parser and returns the
parameter name Set (or undefined when unknown / unsupported / has
rest args / function not found). DynamicInput uses that and keeps
the previous params in flight while the next parse is computing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(dynselect): support deployed helpers in smart retrigger
Add getHelperEntrypointArgs which dispatches on HelperScript.source:
inline parses immediately; deployed fetches the script (or the flow's
inline dyn-select code) once and caches per (workspace, kind, path,
entrypoint). Without this the /scripts/get/* run view fell back to
the full-args comparison and still retriggered on unrelated fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(dynselect): zero-arg helpers report empty deps, not unknown
Codex review flagged that a valid zero-parameter entrypoint was being
treated as "couldn't determine signature" and falling back to the
full-args comparison. Distinguish "function found with no params" from
"function not found" via the parser's auto_kind field — only the
latter sets it, so empty args + auto_kind=null means a real zero-arg
helper and we return an empty Set (no retrigger on unrelated fields).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ensureHandle was doing a post-hoc `if (h.draft === undefined) h.draft = baseline`,
which relies on the saveInitialValue=false skip to swallow that seeding
write. Hand the baseline to `UserDraft.use({ defaultValue })` instead —
useLocalStorageValue uses it as the initial $state value when localStorage
is empty, so lastSerialized is correct out of the gate and no setter call
is needed.
Earlier commits in this PR wired the resource and variable editors to a
single multi-workspace bundle stored under the user's session workspace
key — which mixed workspaces in one localStorage entry and required a
custom multi-key fix-up pass to persist edits for other workspaces.
Reset both editors to their pre-PR shape and apply the minimal change:
the per-workspace `Record<string, ResourceState>` (resp. `VariableState`)
becomes `Record<string, UserDraftHandle<…>>`, with one handle per
workspace created via `UserDraft.use(…, { workspace: ws })`. The handle
keys its own localStorage entry under that workspace, so cross-workspace
edits stay cleanly separated and reactivity flows through the handle's
`draft` accessor — `bind:` on form fields just works.
Adds `manualRelease: true` + `handle.release()` to `UserDraft.use` so
the editors can register handles lazily inside an effect (Svelte 5
forbids `onDestroy` outside component init). The editors register a
single top-level `onDestroy` that releases every collected handle.
After a successful save, the per-workspace autosave is cleared via
`UserDraft.remove(itemKind, path, { workspace })`.
Replace the script editor's toast-based "Discard / Show diff" pattern
with a dedicated modal that surfaces *why* the local autosave is out
of date: a new DB draft on the server, or a new deployed version.
Adds `checkStaleness` (UserDraftMeta vs current backend revs, draft-rev
priority) and a `setMeta({ force: true })` mode so the "Keep current
draft" acknowledgement persists even when it happens to be the
entry's first state mutation — under `saveInitialValue: false` an
ack-only setMeta would otherwise be skipped and the modal would
re-fire on next mount.
The modal lives at LocalDraftStaleModal.svelte; the script editor
wires it as a template for the remaining editors. Other editors
(flows, apps, raw_apps, resources, variables, triggers) still use
the previous toast pattern and will be migrated in follow-up
commits.
Conflict resolution kept the UserDraft persistence on this branch and
incorporated the picker-navigation improvements from main:
- editPathFor / invalidate calls for workspace picker support
- loadFlowToken / loadAppToken / loadScriptToken stale-load guards
- onNavigate prop wiring
Dropped the legacy localStorage `flow-{path}` / `rawapp-{path}` /
`app-{path}` autosave paths that main still carried — they are
superseded by UserDraft (key `userdraft/w/{ws}/{kind}/{path}`).
Dropped main's `decodeState`/window.location.hash autosave for the
script editor for the same reason. The replaceStateFn prop that main
re-added to ScriptBuilder is also dropped (no longer exists on the
component).
Pre-existing typecheck errors in InstanceSetting.svelte and
useJobsLoader.svelte.ts come from main and reflect a stale
$lib/gen client — unrelated to the merge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The schedule_ prefix grouped all the trigger editors under what looked
like a "scheduler" namespace; trigger_ is what these actually are
(triggers — including the cron-style schedule). Mechanical rename
across UserDraftItemKind, every trigger editor's UserDraft.save/get/
remove calls, and the one test that asserted on the localStorage key.
Behaviour-only impact: existing localStorage keys under
userdraft/w/{ws}/schedule_{kind}/{path} from older builds will be
ignored on next open (no schema migration). Users will lose any
unsaved trigger drafts persisted before this change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same pattern, keyed on schedule_schedule. ScheduleEditor doesn't track
an originalConfig (its saveDisabled doesn't compare against a baseline)
so ordering is simpler — initialConfig snapshotted from backend, local
autosave overlaid after.
This completes UserDraft wiring across all 11 trigger editors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same pattern: persist getSaveCfg() on edits, overlay local autosave
in openEdit (with initialConfig/originalConfig snapshotted from
backend first), drop the entry on successful update.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same pattern as the Kafka trigger: persist getSaveCfg() on every edit,
overlay any local autosave on top of the backend value when openEdit
loads the trigger (with initialConfig/originalConfig snapshotted from
backend first so hasChanged correctly reports the overlay as unsaved),
drop the entry on successful update.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same pattern as the Sqs trigger: persist getSaveCfg() on every edit,
overlay any local autosave on top of the backend value when openEdit
loads the trigger, drop the entry on successful update.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Persist the trigger's getSaveCfg() output to
userdraft/w/{ws}/schedule_sqs/{path} on every edit, overlay any
existing local autosave on top of the backend value when openEdit
loads the trigger, and clear the entry on successful update.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: add EditableInput component
* feat: add EditorHeader for flow editor with file picker entry point
* feat: WorkspaceItemPicker for editor header navigation
* feat: clickable breadcrumb in EditorHeader, scoped picker keyboard nav
* fix: reload flow on URL change and reset highlight in search mode
* feat: editor header layout polish and trigger removal
* feat: apply EditorHeader to script, app, and raw app editors
* fix: show generated initial path for new apps in EditorHeader
* fix: align EditorHeader new-app path with draft drawer's Path component
* fix: read page.params.path in loadApp to reload on URL change
* fix: remount AppEditor when navigating between apps
* fix: clear app/files on URL change so editor remounts with fresh data
* fix: route picker selections to /apps/edit or /apps_raw/edit based on raw_app
* fix: drop Save button from path popover; defer rename to deploy
* refactor: drop pathPopoverContent snippet, bind newEditedPath directly
* refactor: dedupe editor header plumbing (editPathFor, userPathPrefix, breadcrumb snippet)
* fix: freeze breadcrumb during path edit so popover doesn't drift
* fix: drop spinner from path dependency-check; render nothing when no usages
* fix: swallow 404 in checkFlowOnBehalfOf so renaming a flow doesn't toast
* Revert "fix: swallow 404 in checkFlowOnBehalfOf so renaming a flow doesn't toast"
This reverts commit 82dec462ae.
* refactor: drop moveRenameManager dep from EditorHeader; pass onBehalfOfEmail as prop
* refactor: replace breadcrumb-snapshot effect with open/close setter
* refactor: drop unused dirtyPath state from EditorHeader
* fix: surface Path validation error in pen popover
* fix: decouple Path validation error from hideFullPath toggle
* refactor: use InputError for path validation message (slide transition)
* fix: re-derive Path meta from external path changes (sibling sync)
* docs: note Path's meta could be replaced with function-form bindings
* fix: 'Exit & see details' uses deployed path, not live store
* fix: undo/redo shortcut uses shiftKey instead of fragile case-match
* fix: type errors and keep edit pen visible while popover is open
* chore: remove unused meltComponents/Accordion wrapper
* fix: focus search input on picker open via popover openFocus selector
* fix: refocus picker search input on every popover open
* fix: pre-seed picker loaded state from cache so accordion opens at the right place
* fix: breadcrumb and picker track savedPath, not draft-renamed live path
* fix: inject current draft item into picker so breadcrumb scope isn't empty
* docs: add component-level and prop-level docs to EditableInput
* feat: warn that a deploy is needed when path is edited on a saved item
* feat: show same path-change-needs-deploy message in flow/script settings
* fix(flows): persist draft-renamed path through reload and dirty check
* fix: include path in unsaved-changes diff so renames trigger the modal
* feat: nested folders in picker tree and breadcrumb
* fix: per-segment popover state so switching breadcrumbs closes the previous one
* refactor: replace accordion picker with drill-through picker
* refactor(picker): review fixes, drill polish, and breadcrumb collapse
* fix(picker): review fixes — banned bindable, drop sibling-sync, load races, mouse highlight
* fix(picker): smooth-scroll highlighted row into view on open
* fix(picker): second-pass review fixes — load races, cache invalidation, breadcrumb/picker a11y, raw_app routing
* fix(RowIcon): apply size prop to resource_type and fallback divs
* feat(picker): add cross-kind 'All' root; deeper breadcrumb segments open there
* fix(picker,editor): third-pass review — search loading state, pen autofocus, allow empty summary, a11y
* fix(picker,editor): fourth-pass review — own check, flowbuilder arrows, editableinput double-save, customui.path gate, invalidate races
* fix(editor,picker): codex review — fresh URL state per load, granular whitelabel topBar gates
* fix(flows): clear localStorage in auto-reload to break URL-state loop
ResourceEditor and VariableEditor can stage edits for several target
workspaces in a single drawer session (see deployTo / states[ws] map).
The previous UserDraft wiring only persisted states[$workspaceStore] —
the user's session workspace — so any edit made under a different
target workspace tab disappeared on refresh.
Persist the entire `states: Record<wsId, State>` bundle as the draft
value instead. On lazy-fetch we pick the local state for that ws if
present and divergent from the backend; on bootstrap for new
resources/variables we restore states for every workspace the user
had staged. The localStorage key still lives under the user's session
workspace via UserDraft, but its contents now cover all target
workspaces from that session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(frontend): branch download UI on shouldDownloadViaClient instead of intercepting in onclick
When OpenAPI.TOKEN is set, several download links rendered an `<a href>`
to the API and relied on an `onclick` handler to call `e.preventDefault()`
and route the request through `downloadViaClient`. This is fragile in
embedded contexts (e.g. the whitelabel React SDK) where Svelte's hydrated
event listener may not intercept the click in time, so the browser
follows the unauthenticated `href` straight to the API.
Mirror the drawer pattern already used in `LogViewer` and
`FlowStatusViewerInner`: render a `<button>` calling `downloadViaClient`
when `shouldDownloadViaClient()` is true, and fall back to the plain
`<a href download>` otherwise. Affects the LogViewer top bar, the large-
result download in DisplayResult, the inline S3 link in ObjectViewer,
the CSV link in ParqetCsvTableRenderer, and FileDownload.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(frontend): treat custom HEADERS / basic auth as token equivalents in shouldDownloadViaClient
Customers wiring the SDK with cookie-bypass auth via custom request
headers (e.g. `OpenAPI.HEADERS = getAuthHeaders()` returning a Bearer
header) had `shouldDownloadViaClient()` return false because it only
checked `OpenAPI.TOKEN`. The plain `<a href download>` branch then
followed the link without those headers, so authenticated downloads
silently degraded to the cookie path (or failed when there is no cookie).
Widen the check to any non-cookie auth: TOKEN, HEADERS, or USERNAME
(basic auth). Route `downloadViaClient` through the generated client's
`getHeaders` so all configured auth schemes are applied consistently
instead of hand-building an Authorization header for TOKEN only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: add global ai mode plan
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: add global ai draft mode
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: scope global ai mode to scripts and flows
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: simplify global ai workspace item shape
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: split global ai write tool into per-type tools
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: add global ai schedule and trigger workspace item tools
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: add dev-only /global_drafts route to inspect ai draft store
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: add edit_script and patch_flow_json global ai tools
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: add deploy_workspace_item global ai tool with confirmation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: emit open-resource action card after deploy_workspace_item
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: add delete_workspace_item global ai tool with confirmation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* chore(system_prompts): emit RESOURCES_BASE and resource/variable zod schemas
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: add global ai resource and variable workspace item tools
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: search_resource_types uses listResourceType to avoid embedding feature dep
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Revert "fix: search_resource_types uses listResourceType to avoid embedding feature dep"
This reverts commit 6d1d19514a.
* feat: emit open-resource action card for variable and resource deploys
* feat: add global ai raw app workspace item tools
* feat: split raw-app prompt into chat-only authoring and cli prefix
* feat: add init_app global ai tool to scaffold raw apps from templates
* fix: pass write_flow value as JSON string for gemini compat
* refactor: hoist countExactMatches and applyExactReplace to chat/shared
* refactor: extract editableFlowJson module shared with global mode
* fix(global): preserve flow schema and groups across draft and deploy
* feat: extract inline scripts from flow reads and patches in global mode
* refactor: add findAndReplace helper for match-validated text patches
* refactor: extract getInlineRunnableContent helper for app file tools
* refactor: extract assertNotGeneratedAppFile guard for /wmill.d.ts
* feat: gate global ai mode behind localStorage flag for dev rollout
* chore: bump svelte to ^5.55.5 in raw app template (sync with main)
* fix: isolate global ai draft rollout
* fix: preserve global ai deploy metadata
* fix: harden global ai draft tools
* chore: remove global ai plan doc
* fix: align raw app prompt guidance
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* [ee] feat(license): offline (URL-bound) license keys
Offline keys are a 4-segment variant for air-gapped customers — no
phone-home, embedded seat/CU caps, locked to the instance's base_url.
Existing 3-segment online keys are unchanged.
Companion PRs:
- windmill-labs/windmill-ee-private (full design + EE impl)
- windmill-labs/windmill-customer-service (issuance + portal)
- windmill-labs/windmill-cf-worker-keygen (signing)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [ee] refactor(license): bind offline keys via instance hash; simpler CU enforcement
- /settings/license_status now surfaces an `instance_hash` superadmins share
with support when requesting an offline key
- OfflineMetadata: `hash` replaces `base_url`; OfflineCapStatus reports
`current_cu` (last 2min) and drops the grace-period fields
- verify_license_key now takes a db so EE can recheck the hash
- InstanceSetting.svelte: hash copy-block + simpler status panel
- Bump ee-repo-ref
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [ee] chore(license): bump ee-repo-ref
Pulls in the current_cu clamp + prod public key restoration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [ee] refactor(license): split instance_hash endpoint; minimal cap UI; restore workers expiry toast
- `instance_hash` is no longer part of /settings/license_status responses; it
lives at GET /settings/instance_hash (super-admin only) so it isn't re-emitted
on every status poll. The UI doesn't show it — admins fetch it explicitly when
requesting a key from support.
- InstanceSetting offline cap UI is now two compact green/red status lines
(Seats X.X/Y and CUs X.X/Y) placed above the action buttons, matching the
existing "Latest key renewal" badge style. The block-panel is gone.
- "Latest key renewal" line and the "Renew key" button are now hidden when an
offline key is loaded (renewal is server-disabled for offline keys).
- Restore parseLicenseKey + checkLicenseExpiration toast on /workers
(works for both 3- and 4-segment keys).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [ee] chore(license): bump ee-repo-ref
Pulls in the plain-SHA256 instance hash + stats_ee revert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [ee] chore(license): bump ee-repo-ref
Picks up the alert wording change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [ee] chore(license): bump ee-repo-ref
Picks up the instance_uid cache so the periodic verify_license_key cycle
no longer hits global_settings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [ee] refactor(license): rename /settings/license_status → /offline_license_status
The endpoint was only used by the offline-license UI; the other fields it
returned (license_key_id, license_key_valid, kind, offline metadata) were
unused. Rename to clarify scope and flatten the response — it now returns
just the OfflineCapStatus (or null when no offline license is loaded).
Frontend uses `offlineCapStatus != null` as the "is offline" check.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [ee] fix(ci): regenerate sqlx cache for the inline worker_ping query
After reverting unused stats_ee helpers (fetch_worker_pings*), the
inline `sqlx::query_as!(WorkerPingRecord, ...)` in get_stats_payload
lost its cache entry — CI's check_ee_full + cargo_test were failing
under SQLX_OFFLINE=true with E0282 type-inference errors.
Re-running update_sqlx.sh regenerates the cache file under its
current hash and prunes a couple of stale entries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [ee] fix(license): address cubic-bot review
- get_offline_license_status: propagate enforce_offline_caps errors as 500
instead of swallowing into a "no offline license" (Option::None) response
- canonical_base_url: rewrite the doc to match the actual fallback behavior
(lowercase + trailing-slash strip on URL parse failure); the original
cross-service contract is gone since the customer-service no longer
canonicalizes (treats the instance hash as opaque)
- check_seat_cap_for_new_user: take an email and short-circuit when the
email is already in `usr ∪ workspace_invite` so net-zero invite upserts
and invite→user transitions aren't spuriously blocked at cap. Mirrors
the dedup rule the count itself uses.
- Bump ee-repo-ref to pull in the EE-side change
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [ee] chore(license): bump ee-repo-ref
Picks up the exact-delta seat-cap check (replaces the simple existence
short-circuit). Regenerates the new sqlx cache for the bool_and query.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [ee] fix(license): propagate get_instance_hash errors; bump ee-repo-ref
- get_instance_hash: replace `.ok().flatten()` with map_err+? so DB errors
during instance_uid lookup surface as 500 instead of silently returning
`{"instance_hash": null}` (same pattern get_offline_license_status already uses)
- Bump ee-repo-ref to pull in the enforce_offline_caps cached-state preservation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to c6cd1afe2d9e04809b30751cd1687b28a65e62b1
This commit updates the EE repository reference after PR #566 was merged in windmill-ee-private.
Previous ee-repo-ref: a6d91016ae0d43c46604313aecae3aa9c778c8e0
New ee-repo-ref: c6cd1afe2d9e04809b30751cd1687b28a65e62b1
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* chore: narrow secret-file Read deny rule to dotfiles/extensions
* feat(vault): configurable JWT auth mount path and fix setup docs
* chore: bump ee-repo-ref for vault jwt mount path
* chore: bump ee-repo-ref after rebase onto EE main
* chore: update ee-repo-ref to a1cd60b54e8595b4e5ce6b654e675e4bbe2253b2
This commit updates the EE repository reference after PR #567 was merged in windmill-ee-private.
Previous ee-repo-ref: c274f233a0ebb54afa296c3db15ff330e1baebcf
New ee-repo-ref: a1cd60b54e8595b4e5ce6b654e675e4bbe2253b2
Automated by sync-ee-ref workflow.
---------
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
VariableEditor persists the current workspace's edit state via
UserDraft.save on every mutation, keyed on editPath ('' for new
variables → in-memory only). Backend fetches now overlay a matching
local autosave when one exists, and initNew() rehydrates from the
in-memory empty-path entry so opening a fresh "Add variable" drawer
keeps any unsaved work from the previous open. After a successful
save we drop the corresponding entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ResourceEditor registers a UserDraft.use<ResourceState> handle keyed
on the initialPath (empty for new resources, in-memory only). A
$effect deep-tracks the current workspace's edit state and forwards
mutations to the handle; on bootstrap and lazy backend-fetch the
local autosave wins over the backend value when they diverge. After
a successful save() we call UserDraft.remove so the local autosave
doesn't outlive the deploy. Cross-workspace deploys always start from
the live backend value rather than the local draft.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/apps_raw/edit owns the canonical raw-app state (files, runnables,
data, summary) in four $state vars; a single $effect deep-tracks them
and forwards the bundle to a UserDraft.use<RawAppDraft> handle so each
mutation tick persists at userdraft/w/{ws}/raw_app/{path} (deduped by
useLocalStorageValue's serialized check). On load the route overlays
the local autosave on top of backend.draft/deployed and offers a
"Discard / Show diff" toast when they diverge; matching local entries
are silently dropped. Deploy, save-as-draft rename, restore-draft and
restore-deployed each call UserDraft.remove on the route path.
/apps_raw/add keeps the same shape (UserDraft.use with empty path)
so the draft is in-memory only and we drop it explicitly when the
initial save creates the real path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AppEditor registers a UserDraft.use<App> handle for its current path
(empty path for /apps/add stays in-memory) and a single $effect
deep-tracks the internal stateApp and forwards every mutation to the
handle. useLocalStorageValue's lastSerialized check then dedupes the
actual localStorage writes per tick, so even fast drag/resize loops
only persist when the JSON output really changes.
/apps/edit overlays a local autosave from UserDraft.get on top of the
backend value when one exists, with the existing "Discard / Show diff"
toast wired to UserDraft.remove. Deploy, save-as-draft, restore-draft
and restore-deployed all call UserDraft.remove on the relevant path,
including the JSON editor save paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>