* feat: export audit logs to dedicated object store folder
* fix: gap-free audit export via snapshot-xmin gate and stable object keys
* test: add integration test for audit log object store exporter
* fix: cursor audit export on snapshot xmin to prevent id-leapfrog loss
* fix: protect audit s3 checkpoint from config sync and bound export interval
* fix: anchor audit s3 checkpoint at enable time to not skip first-window rows
* fix: anchor first audit export at the enable transaction's xid
* fix: use epoch timestamp floor on first audit export run to not drop old backlog
* fix: anchor audit export at startup for env-var enable path
* fix: anchor audit export via enabling-txn snapshot xmin trigger
* fix: bound the bootstrap audit export to MAX_XID_INTERVAL per tick
* refactor: store audit export cursor in background_task_state, add status endpoint
* docs: align store_audit_logs_s3 setting text with the actual enable-boundary contract
* [ee] refactor: move audit s3 export core logic to EE, gate on Enterprise license
* chore: update ee-repo-ref to ec3cd353245e1cdf6a290528dbd7f2ac2498386c
This commit updates the EE repository reference after PR #579 was merged in windmill-ee-private.
Previous ee-repo-ref: 4ffc6d5f874e64d7dc4a147b4e73baa6c44867a5
New ee-repo-ref: ec3cd353245e1cdf6a290528dbd7f2ac2498386c
Automated by sync-ee-ref workflow.
---------
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix: reject path traversal in MCP endpoint path parameters
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: narrow MCP path-param validator to structural escapes only
Codex review: rejecting whitespace/`:`/`@` regressed legitimate
Windmill paths (app paths with spaces, email-style usernames like
u/admin@windmill.dev/...). These are ordinary path-segment data in an
absolute URL and cannot redirect the request. Reject only structural
escapes: control chars, `\`, `%`, `?`, `#`, and `.`/`..`/empty segments.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: enforce jobs:run scope on job preview and inline endpoints
Preview/inline endpoints (run/preview, run/preview_bundle, run/preview_flow,
run/dynamic_select inline) execute arbitrary request-supplied code but only
checked folder/namespace read access, which is a no-op when path is null. A
token scoped to a specific script/flow could escape its scope and run any
code. Add a jobs:run scope check, matching other arbitrary-execution
endpoints. Advisory GHSA-vxc5-w28p-m9xw.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: scope-check dynamic_select flow branch and inline preview
Address CI review: the dynamic_select Deployed{Flow} branch ran a deployed
flow's dynamic-select code without any scope check (only the Script branch
delegated to a scope-checked handler), and run_inline_preview_script executed
request-supplied code with no in-handler scope check. Add jobs:run:flows:{path}
to the flow branch and jobs:run to inline preview; correct the misleading
comment. Expand regression tests (preview_flow case, assert success for the
broad-token case). Advisory GHSA-vxc5-w28p-m9xw.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: remove preview scope enforcement test after local validation
The regression test passed locally (3/3) and validated the fix end-to-end;
removed from the PR per maintainer preference. Advisory GHSA-vxc5-w28p-m9xw.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: constrain unauthenticated get_public_resource to app_theme resources
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: remove get_public_resource regression test
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The draft upsert was `ON CONFLICT (...) DO UPDATE SET value = EXCLUDED.value`,
so subsequent draft writes left `created_at` frozen at the first INSERT.
The frontend's UserDraft staleness check reads that timestamp as
`remoteDraftRev`; with it frozen, an updated remote draft looked
identical to the originally-baselined one and the "newer draft was
saved on the server" modal never fired.
Touch `created_at` on conflict too. The column's semantic widens from
"first write time" to "last write time", which is what every reader of
the field actually wants — the staleness signal is the only consumer.
SQLx offline cache regenerated to match the new query text.
* 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>
* 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>
Variable
- Add `edited_at TIMESTAMPTZ NOT NULL DEFAULT now()` + `edited_by VARCHAR(50)` to the `variable` table (parity with `resource`); set them on INSERT and on every UPDATE.
- Surface them on `ListableVariable` so `getVariable` / `listVariable` return them.
DB drafts (script, flow, app/raw_app)
- The `*WithDraft` endpoints now also return `draft.created_at` as `draft_created_at`. The draft value alone wasn't enough to tell whether a teammate (or another tab) had pushed a fresh draft while local autosave was in flight; the new field is the staleness signal.
- Wired in `get_script_by_path_w_draft` (`ScriptWDraft.draft_created_at`, including the `prefetch_cached` forwarding), `get_flow_by_path_w_draft` (`FlowWDraft.draft_created_at`), and `get_app_w_draft` (`AppWithLastVersionAndDraft.draft_created_at`). OpenAPI updated to match.
The frontend will read these in a follow-up to implement the local-draft staleness check; this commit only widens the API surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <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>
* docs: refine windmill ai refactor plan
* refactor: move ai sse plumbing to windmill-ai
* refactor: remove ai re-export shims
* fix: update ee ai memory ref
* chore: update ee-repo-ref to d3bc7fa85195b46b7a38d43c2f806520bf8b5454
This commit updates the EE repository reference after PR #560 was merged in windmill-ee-private.
Previous ee-repo-ref: ff35bf7cc198e13884b33654e1d6dbd8a8b314d3
New ee-repo-ref: d3bc7fa85195b46b7a38d43c2f806520bf8b5454
Automated by sync-ee-ref workflow.
---------
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat: add workspace-specific flag for resources and variables
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove set_ws_specific endpoint and fix rust-client compilation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: fall back to workspace name for ws_specific file naming
When wsNameForFiles is not set (no wmill.yaml workspace config),
ws_specific items would not get workspace-suffixed filenames during
pull. Now falls back to workspace.name/workspaceId.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use workspace ID instead of CLI name for ws_specific file naming
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: pass workspace ID fallback to elementsToMap for ws_specific push
Without this, workspace-specific files (e.g., a.admins.resource.yaml)
were not recognized during push when no wmill.yaml or git branch was
available, causing spurious deletions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ui nits
* nit
* Fix variable edit when only editing ws_specific
* mark_linked_variables_ws_specific
* Helper label
* Support json format alongside yaml
* Fix file naming push/pull asymetry & ws_specific orphans
* Revert all CLI diffs
* CLI now appends the remote ws_specific list to the local specificItems
* UI for Env switcher
* Refactor Resource/Variable editors to use dumb component
* Refactor side effects
* Editor works with multi workspaces
* Fix can_save
* Fix As JSON
* nit
* UI nits
* list_ws_specific_versions as pl sql function to avoid round trips
* UI Nits
* Per-workspace version read-only check
* fix: reset session context in list_ws_specific_versions to prevent RLS leakage
The function calls set_session_context() in a loop. Although SET LOCAL is
transaction-scoped (so settings revert at autocommit), defending against
the function being invoked inside a longer outer transaction:
- wrap the loop in a sub-block with EXCEPTION WHEN OTHERS that resets
the session to a deny-default (windmill_user, empty session.* GUCs)
before re-raising,
- on the happy path, reset to the same deny-default at the end of the
function.
* feat: audit auto-marked ws_specific variables
When a resource is saved as ws_specific, every variable referenced via
$var: inside its value is auto-INSERTed into ws_specific. Previously
this happened silently. Now:
- mark_linked_variables_ws_specific takes the authed user,
- the INSERT uses RETURNING path so we know exactly which variables
were freshly flipped (not the ones already ws_specific),
- each newly flipped variable gets a 'variables.set_ws_specific' audit
entry pointing at the resource that triggered it.
* perf: skip mark_linked_variables_ws_specific when nothing relevant changed
update_resource was calling mark_linked_variables_ws_specific on every
save when the resource was ws_specific, even on a description-only or
label-only edit. Gate the call on `ns.value.is_some() || ns.ws_specific
== Some(true)` so we only re-mark when the $var: refs could actually
have changed or ws_specific was freshly enabled.
* docs: explain asymmetric ws_specific toggle in resource tooltip
Enabling the resource's 'Workspace specific' toggle silently marks
every variable referenced via $var: inside the value as ws_specific,
but disabling it does not un-mark those variables (they may be
referenced by other resources). Surface this in the tooltip so users
know what to expect.
* fix: surface non-404 errors when fetching ws_specific items in CLI sync
mergeWsSpecificFromServer was catching every error from listWsSpecific
and logging it at debug. That's correct for old servers without the
endpoint (404), but a 401/403/network failure would silently produce an
incomplete sync. Now distinguish 404 (debug, expected) from everything
else (warn with status + message) so users notice when the merge fails
for real reasons.
* perf: collapse compare_two_variables presence checks into one round-trip
The early-return path was issuing four sequential EXISTS queries
(ws_specific × {source, fork}, variable × {source, fork}). Combine
them into a single SELECT so the per-variable diff cost drops ~4x.
* sqlx prepare
* docs: clarify has_sql_updates invariant in update_variable
The else branch of the npath resolution is only reachable for non-rename
edits (labels-only, ws_specific-only) because ns.path being Some always
forces has_sql_updates=true at the top of the function. Add a debug_assert
and a comment explaining the invariant so a future change that decouples
ns.path from has_sql_updates trips immediately. Also use `path` directly
instead of unwrap_or_default-ing ns.path, since we know it's None here.
* chore: drop redundant ws_specific type augmentations
ListableResource and ListableVariable from $lib/gen now include
`ws_specific?: boolean` after the openapi.yaml additions in this
branch were regenerated. The intersection types in resources/+page
and variables/+page were duplicating the field — drop them.
* Put WsSpecificVersions toggle in top drawer bar
* nit size
* feat: detect local-only ws_specific items on sync push
When wmill.yaml lists a resource/variable in specificItems but the
remote isn't yet marked ws_specific for that item, sync push silently
dropped the flag because:
1. file-content diff alone never noticed (ws_specific is metadata, not
YAML body) — push{Resource,Variable} were never called for those
items;
2. even when called, isSuperset(local, remote) returned true and the
early-return skipped the API call.
Now:
- mergeWsSpecificFromServer returns the raw server list alongside the
merged config so push can compare 'in local' vs 'in server';
- a new computeWsSpecificFlagOnlyPushes helper walks the local file map,
finds ws_specific-flagged paths absent from the server list, and the
push function injects them as synthetic 'edited' changes (same before
and after content) so the standard display + apply pipeline picks
them up;
- push{Resource,Variable} no longer early-return when content matches
but the ws_specific flag differs.
Pull is unaffected — only the push-side caller of mergeWsSpecific takes
the new (merged, serverItems) tuple.
* getDeployTo for selected ws
* refactor: ws_specific kind handling, support .json files
The ws_specific helpers had two warts:
1. computeWsSpecificFlagOnlyPushes hardcoded `.resource.{yaml,json}` /
`.variable.{yaml,json}` magic strings, even though the existing
getTypeStrFromPath / removeType helpers already do that work and
already cover both extensions.
2. isSpecificItem / isItemTypeConfigured only matched `.yaml` paths,
so users with opts.json local files got no specificItems coverage
at all — patterns from wmill.yaml (and from mergeWsSpecificFromServer)
are expressed with `.yaml`, and a `.json` file never matched.
Changes:
- Replace WS_SPECIFIC_KIND_MAP (a closed enum of resource+variable)
with configKeyForItemKind, a generic kind→SpecificItemsConfig key
mapping. Triggers fold into 'triggers' via the `_trigger` suffix,
so adding a kind to the backend's list_ws_specific_versions doesn't
require a CLI change.
- mergeWsSpecificFromServer now appends `${item.path}.${item.item_kind}.yaml`
through the same helper.
- computeWsSpecificFlagOnlyPushes uses getTypeStrFromPath + removeType,
gated by configKeyForItemKind. No more magic strings.
- isSpecificItem and isItemTypeConfigured normalize trailing `.json` to
`.yaml` once at the entry, so a single set of patterns covers both
extensions for the same logical item.
* refactor: dedicated change type for ws_specific flag-only pushes
Previously the sync push code injected a synthetic 'edited' Change with
before === after to nudge the apply loop into calling pushResource /
pushVariable for ws_specific-flag-only diffs, and a guard inside those
two functions skipped the early-return when the flag differed. The
contract was implicit and easy to break — any future 'skip identical
edits' optimization in the change pipeline would silently drop these
pushes.
Replace with an explicit Change variant:
type WsSpecificFlag = {
name: 'ws_specific_flag';
path: string;
kind: string;
wsSpecific: boolean;
};
The push apply loop now has a dedicated branch for it that calls
wmill.updateResource / updateVariable with just the ws_specific flag.
prettyChanges renders it on its own line. The dry-run JSON output picks
it up via the existing change.name / change.path passthrough.
The defensive wsSpecificMatches check inside push{Resource,Variable} is
no longer needed (sync push doesn't go through them for flag-only
diffs) and is reverted.
* drop folders
* feat(cli): warn on remote ws_specific items missing from local config
When 'wmill sync pull' fetches the server's ws_specific list, items the
server marks as ws_specific but that aren't matched by the local
wmill.yaml's specificItems patterns now produce a warning. The merge
already preserves correctness (those items are still treated as ws_specific
during this pull), but the user's config drifts from the remote — and a
later push from another machine without that config would push the item
as non-ws_specific. Surface the drift so the user can update wmill.yaml.
Also filter ws_specific_flag changes out before preCheckPermissionedAs
(it expects added/edited/deleted only and they have no content payload
so on_behalf_of resolution doesn't apply).
* fix(cli): scope ws_specific drift warning to items in this pull's changes
Previously the warning iterated every ws_specific item the server returned,
producing log spam for items unrelated to the current pull (items that
exist locally with no change, or items the user has nothing to do with
this round). Move the loop after compareDynFSElement and only warn for
items whose path appears in the changes list — i.e., items the user is
actually pulling right now.
* fix: clean up linked-side ws_specific rows on resource/variable delete
Three places left orphaned ws_specific rows behind:
1. delete_resource deleted the resource's own ws_specific row and the
linked variables, but never the ws_specific 'variable' rows that
mark_linked_variables_ws_specific had auto-inserted for those
variable paths.
2. delete_variable deleted its own ws_specific row and the linked
resource at the same path, but never a ws_specific 'resource' row at
that path.
3. delete_resources_bulk didn't even cascade to linked variables, let
alone clean up their ws_specific rows.
A new resource or variable later created at one of those paths would
silently inherit a stale ws_specific flag — list_ws_specific would
report it as workspace-specific, workspace diffs would treat it as
'no changes', and CLI sync would skip it.
Fix:
- delete_resource: DELETE FROM ws_specific WHERE item_kind = 'variable'
AND path = ANY(linked_var_paths) before the linked-variable delete.
- delete_variable: DELETE FROM ws_specific WHERE item_kind = 'resource'
AND path = path before the linked-resource delete.
- delete_resources_bulk: collect $var: refs from each bulk-deleted
resource (mirror of single delete), then delete ws_specific 'variable'
rows AND the variable rows themselves. Brings bulk delete in line with
single delete semantics, including the orphan cleanup.
* fix: gate list_ws_specific by resource/variable RLS
The endpoint queried ws_specific directly under user_db, but ws_specific
itself has no per-item RLS — only a workspace-level column. Any workspace
member could enumerate every ws_specific path including those in folders
they lack read access to (e.g. f/finance/prod_db_creds), revealing path
existence that list_resources / list_variables would have hidden.
Add EXISTS clauses against resource and variable so the same path-based
RLS policies that govern those tables (see_own / see_member /
see_extra_perms_user / see_extra_perms_groups / see_folder_extra_perms_user)
also gate visibility here. The user transaction already establishes the
session context; the joins make the policies apply.
* only resources and variables
* fix(cli): make workspace-specific path mapping handle .json files
isSpecificItem() was extended to normalize .json -> .yaml so .json
files could be matched against patterns, but the surrounding helpers
remained yaml-only:
- toWorkspaceSpecificPath only mapped folder.meta.yaml / settings.yaml
/ .X.yaml — a foo.resource.json went through unchanged, so the
workspace-specific filename was never produced.
- fromWorkspaceSpecificPath only matched .yaml extensions — pushing
foo.dev.resource.json could not map back to foo.resource.json.
- isCurrentWorkspaceFile / isWorkspaceSpecificFile regexes ended in
\.yaml$, missing every branch-specific .json file.
Replace the literal '.yaml' anchors with '(yaml|json)' alternations,
preserve the actual extension on round-trips, and rename the helper
buildYamlTypePattern -> buildItemTypePattern (it never had anything
extension-specific in it). getFileTypeSuffix now returns the matching
suffix for either extension. Changed:
- getFileTypeSuffix
- toWorkspaceSpecificPath / fromWorkspaceSpecificPath
- isCurrentWorkspaceFile / isWorkspaceSpecificFile
- isTriggerFile / isScheduleFile
isItemTypeConfigured / isSpecificItem don't need touching — their
checks run after normalizeJsonToYaml(), which already collapses both
extensions to .yaml at the entry.
* fix: create_resource?update_if_exists=true honors ws_specific=false
The upsert path matched on `unwrap_or(false)`, so an explicit
`ws_specific: false` and an absent flag were indistinguishable — both
fell through with no DELETE on the existing ws_specific row. Callers
trying to clear the flag via PUT-with-update_if_exists silently saw
their request ignored.
Mirror update_resource's three-way handling:
Some(true) -> INSERT (+ mark linked variables)
Some(false) -> DELETE (only when update_if_exists, since a pure
create has no existing row anyway)
None -> leave the existing flag alone
create_variable doesn't have an upsert path (no ON CONFLICT), so the
same bug doesn't apply there.
* sqlx prepare
* test: cover ws_specific cleanup, RLS filtering, upsert clearing, and CLI .json paths
Backend (backend/tests/ws_specific.rs + fixture):
- test_linked_delete_cleanup: creates a ws_specific resource that
references a variable via $var:, deletes the resource, asserts the
cross-kind ws_specific row for the auto-marked variable is also
removed. Then does the inverse for delete_variable, verifying the
ws_specific 'resource' row at the same path is cleaned by variable
delete.
- test_list_ws_specific_filters_by_rls: admin creates ws_specific items
in u/test-user/ and u/test-user-2/; verifies admin sees both via
list_ws_specific while a non-admin (test-user-2) only sees their own
path — the RLS see_own policy on the joined resource/variable tables
hides the other.
- test_create_resource_upsert_clears_ws_specific: covers the three-way
Option<bool> handling on the upsert path: Some(true) inserts,
Some(false) clears the existing row, None leaves it alone.
CLI:
- specific_items_unit.test.ts: add 14 tests covering toWorkspaceSpecificPath
/ fromWorkspaceSpecificPath / isWorkspaceSpecificFile /
isCurrentWorkspaceFile / isSpecificItem / isItemTypeConfigured for
.json files (variable, resource, trigger, schedule, folder.meta,
settings).
- ws_specific_flag_only_unit.test.ts (new): covers
computeWsSpecificFlagOnlyPushes — emits flag-only changes only for
resource and variable kinds (the backend's list_ws_specific_versions
scope), does not emit for schedules or triggers, returns empty when
serverItems is null (older server), respects existing server entries,
preserves .json extension on filePath.
- Export computeWsSpecificFlagOnlyPushes so it can be unit-tested.
* perf: index workspace_settings.deploy_to for the recursive CTE
list_ws_specific_versions's recursive CTE probes WHERE ws.deploy_to =
r.ws_id every iteration; without an index on workspace_settings.deploy_to
each iteration seq-scans the table — at 10M workspaces with the depth
cap of 32 that's up to 320M row reads per call.
deploy_to is sparse (most workspaces don't deploy anywhere), so a
partial index WHERE deploy_to IS NOT NULL stays small while still
covering every probe. Tucked into the existing migration since the
function and the index ship together.
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [ee] feat(secret-backend): add Workload Identity Federation for Azure Key Vault
Make `client_secret` optional. When omitted, Windmill falls back to
Azure Workload Identity Federation: it reads the projected
service-account JWT from AZURE_FEDERATED_TOKEN_FILE and exchanges it
with Entra ID via `client_assertion`, no long-lived secret stored on
the instance. Same code path covers AKS (workload-identity admission
webhook auto-injects the env vars) and any other Kubernetes cluster
federated to Entra ID (EKS/GKE/self-hosted).
- backend: relax client_secret to Option (already was), update doc
comment + OpenAPI description; the actual auth-branching logic lives
in the EE companion file (azure_kv_ee.rs).
- frontend: drop client_secret/token from canSubmit so saving with an
empty secret is allowed; add inline help under the Client Secret
field pointing to AZURE_FEDERATED_TOKEN_FILE; mark the field optional.
- ee-repo-ref: bump to the EE companion commit.
EE companion: see windmill-ee-private branch azure-keyvault-managed-identity.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [ee] chore: bump ee-repo-ref for blank-client_secret fix
Picks up the EE-side fix (windmill-ee-private c7c0a23) that treats blank
`client_secret` as workload-identity instead of POSTing an empty string
to Entra ID. Addresses Codex review on PR #9061.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to c8d100d74b8de6bd26fc973d5edbd8853d54dd8b
This commit updates the EE repository reference after PR #561 was merged in windmill-ee-private.
Previous ee-repo-ref: c7c0a23459b0e7416a045a279346cc48b30eed32
New ee-repo-ref: c8d100d74b8de6bd26fc973d5edbd8853d54dd8b
Automated by sync-ee-ref workflow.
---------
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>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
* feat(forks): handle triggers and schedules in wmill workspace merge
Closes#9001. Brings CLI parity with the merge UI by routing trigger and
schedule diffs through the existing workspace_diff tally infrastructure
and lifting the deploy logic into the shared windmill-utils-internal
module.
- Backend: extend tally + compare to all 10 trigger kinds + schedule;
new compare_two_trigger_or_schedule helper using to_jsonb minus runtime
ignore set; CompareSummary gains schedules_changed/triggers_changed.
- Operational-state invariant: fork operations never flip target's
mode/enabled. Triggers strip mode/enabled in both UI and CLI deploy
payloads (preserved by is_mode_unspecified on backend). Schedules drop
the setScheduleEnabled mirror entirely on merge — EditSchedule lacks
enabled by design.
- Shared module: DeployKind extended with schedule + per-kind triggers;
DeployProvider gains per-kind dispatch methods.
- Frontend: ~600 lines of client-side trigger-diff machinery deleted;
rows flow through comparison.diffs like every other kind. Diff drawer
returns full GET response stripped of runtime fields, matching backend
semantics. Default selection excludes triggers/schedules (opt-in).
- CLI (merge.ts): per-kind provider, GCP-specific transforms (audience
reset, base_endpoint with /api stripped to match frontend), summary
table rows for Schedules/Triggers, default-deselect mirroring the UI.
- Bumps windmill-utils-internal to 1.5.0 (new exports for trigger
per-kind dispatch); frontend depends on ^1.5.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(enterprise): clarify [ee] prefix applies whenever an EE companion PR exists
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 6ee680c25e3413d928fc22002be6deb118092668
This commit updates the EE repository reference after PR #557 was merged in windmill-ee-private.
Previous ee-repo-ref: ad35a056627656fd426fb19856ea945955d4727f
New ee-repo-ref: 6ee680c25e3413d928fc22002be6deb118092668
Automated by sync-ee-ref workflow.
* fix(forks): preserve target state on merge update, mirror source on create
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(forks): strip server-managed trigger fields and honor --include with --skip-conflicts
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>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
* fix: handle singlestepflow zombies and stop filtering them from runs page
* fix: support singlestepflow in batch_rerun_jobs
Previous PR added singlestepflow to list_selected_job_groups so the BatchReRun
pane shows them, but batch_rerun_jobs_inner still joined on kind = 'script' /
'flow' with j.runnable_id (which is NULL for SingleStepFlow), so the rows were
silently filtered out — user sees the option, click Re-run, gets zero successes.
Mirror the norm_kind CTE projection from list_selected_job_groups inside
batch_rerun_jobs_inner: pull the wrapped runnable type and pinned script hash
from raw_flow.modules[id='a'], cast back to JOB_KIND so the existing handler
dispatch works unchanged. Path-based schema fallback so input_transforms still
resolve at rerun time.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: project singlestepflow in batch-rerun schema lookups
Codex review pointed out two follow-on regressions from the previous fix:
(1) list_selected_job_groups returned schemas with script_hash=null and
schema=null for singlestepflow rows because the inner schemas subquery still
joined runnable metadata via j.runnable_id (NULL for SingleStepFlow). The
BatchReRun pane consumes every selected.schemas entry through
mergeSchemasForBatchReruns / buildExtraLibForBatchReruns, both of which
assume real schema objects.
(2) When use_latest_version=true, batch_rerun_handle_job re-fetched
latest_schema from v2_job filtering jb.kind='script' or 'flow' — neither
matched singlestepflow, so schema came back NULL and every input_transforms
entry silently no-op'd.
Both queries now project singlestepflow rows via raw_flow.modules[id='a'] —
norm_kind for dispatch and effective_hash for the schemas join, plus a
path-based latest-schema fallback so flow-wrapped SSF (no version pinning)
and any SSF whose pinned hash has been deleted still resolve.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: add batch_rerun integration tests, fix SSF hash hex parsing
Adds 11 integration tests against /jobs/run/batch_rerun_jobs and
/jobs/list_selected_job_groups (both endpoints had zero CI coverage).
Tests cover the full 4-kind × 3-mode matrix: regular Script and Flow
(baseline regression for the SQL refactor), script-wrapped and flow-
wrapped SingleStepFlow (regression for the bugs this PR fixes), and a
mixed-kind batch.
Writing the tests caught a real bug in the previous commit: ScriptHash
serializes as a 16-char hex string in raw_flow.modules[a].value.hash
(per the custom Serialize impl in windmill-types/scripts.rs), not as
an integer. The earlier `(m->'value'->>'hash')::bigint` cast worked
on the hand-inserted SQL fixture I'd used for live testing (which
embedded the hash as a raw integer) but failed in production where
all SSF jobs are pushed via JobPayload::SingleStepFlow's serialized
form. Replaced with `('x' || lpad(hex, 16, '0'))::bit(64)::bigint` —
preserves the twos-complement bit pattern so both positive and
negative i64 hashes round-trip correctly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Update SQLx metadata
---------
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>
* fix(flows): inherit flow_env in sub-flow predicates
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(flows): align flow_env lookup with get_root_job_id and tighten gate
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(flows): drop recursive CTE, root_job propagation suffices
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(flows): walk via flow_innermost_root_job to respect imported-flow scope
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(flows): remove flow_env API endpoint, dead code from deno_core era
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor: replace SELECT * with explicit column lists
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* chore: update sqlx offline query cache
* chore: update sqlx offline query cache
* chore: update sqlx offline query cache with EE support
* chore: update sqlx offline query cache, no deletions
* chore: update sqlx offline query cache after rebase
* fix: correct column names in explicit script query lists
- concurrency_limit → concurrent_limit (matches DB column name)
- runnable_settings → runnable_settings_handle (matches DB column name)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: add missing delete_after_secs column to script queries
Also add integration test covering all explicit-column export queries.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test: add workspace export integration test covering all explicit-column queries
Covers tarball_workspace (folder, script, resource, resource_type, variable,
schedule, usr, group_) and the mcp_oauth_client SELECT query from windmill-mcp.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test: add tarball export integration test covering all explicit-column queries
Single test creates one of each entity type and exercises every runtime-checked
explicit-column query in tarball_workspace. Uses archive_type=tar to avoid
zip feature-gate in CI.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* docs: fix stale verification step and CI contradiction in update-sqlx skill
- Regenerate current_files.txt after EE cache restoration so step 4 reports accurate diff
- Scope "Never use SQLX_OFFLINE=true" to local prepare (CI legitimately uses it)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* docs: remove Co-Authored-By from commit skill template
* refactor: extract SCRIPT_COLUMNS const to single source of truth
Replaces 5 duplicated 44-column lists with a shared const in windmill-types.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* 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>