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
Every path that touches a data table's permissions or a principal its roles can
name takes the workspace settings row before it locks anything else, and a
transaction spanning workspaces takes them in `workspace_id` order. One lock
acquired first cannot deadlock, so the rule is stated once on the lock rather
than as an order each handler has to reproduce — the rename can write its thirty
tables in whatever order reads best, and the statements moved around to fake an
order move back.
What waits for the config to commit is now decided from the plan and checked per
role: a request that disables permissions still lists every role, so the set
built from it claimed them all and no drop waited at all. A role the committed
config no longer names is dropped whatever else changed meanwhile, where before
any unrelated edit — a tenant, the default role — made the whole cleanup stand
down and left the login behind with nothing that would ever remove it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
A permissions save ran its whole plan against the data table's database before
writing the config, so an opt-out or a role removal that failed on the Windmill
side afterwards rolled the config back with the roles already gone and their
grants discarded. Only the creates and renames run there now; the drops run once
the config that no longer names them has committed, skipped if another save has
landed in between.
The rename's `group_` rewrite moves after the settings row for the same reason
the rest of it did: group deletion takes them in that order, and the two
overlapping was a deadlock.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
`update_username_in_workpsace` locks one workspace's settings row but wrote
`usr` and `usr_to_group` across every workspace holding that name, so it waited
on principals another workspace's settings row guards — the deadlock the last
commit closed, one workspace over. Both writes are scoped now, and the rename
walks memberships in workspace order.
The deferred role drop holds the settings row while it runs, so its session
carries a statement timeout: the statements run on a database the workspace does
not control, and a lock held there would stall every save behind it.
`PgRoleInventory` is read by the enterprise planner alone, which a community
build compiles without.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
Role names are generated from the data table's name, so one recreated under a
deleted table's name generates the same ones, and its save adopts them. The drop
planned for the deleted table would then take the live one's logins and grants.
It now runs under the settings row it deletes them for, and stands down when
that name is back.
The rename writes `usr` and `usr_to_group` after the settings row, like every
other path that frees or renames a principal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
* chore: stop denying reads of secret files in claude settings
Any Read() deny rule makes Claude Code resolve the file operands of every
Bash command that reads files. A path it cannot resolve, such as one that
follows a cd into a directory the analyzer does not track, escalates to a
permission prompt even under bypassPermissions. A plain recursive grep in
the repo root escalates too, because it could reach .env.
Drop the read rules and widen the write rules to cover the same files, so
secrets still cannot be written through Edit, Write, or a shell redirect.
Reads of those files are no longer blocked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNCupPk2yewQT1JMNjkV8M
* fix: keep the sso group reconciler alive in oauth2-less builds
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W24T1FjQXQ87AoeC3UxWWC
* chore: update ee-repo-ref to d6297e6844dc2aab4745fce328e32ccab508969f
This commit updates the EE repository reference after PR #777 was merged in windmill-ee-private.
Previous ee-repo-ref: eec88486fb2df0ba15998ef285f52fc67af90b1e
New ee-repo-ref: d6297e6844dc2aab4745fce328e32ccab508969f
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Postgres roles are cluster-wide, so two data tables that generate one name share
a login. The name's discriminator was four bytes, which is searchable rather
than merely unlucky: two data table names that collide were found by hand. It
is now sixteen, and a name that is already taken is refused unless this data
table owns the role (enterprise repo).
The database a data table's roles were created in is recorded with them and
checked where the connection is finally resolved. The guards that refuse a
resource edit read the config separately from the save that enables permissions,
and never expand a `$res:` or `$var:` that can repoint the resource just as
well — so they stay as early refusals while the check that holds is the one at
the point of use.
Dropping a deleted data table's roles now runs after the settings transaction
commits, planned before it: `DROP OWNED` discards their grants for good, and a
save that rolls back after that point would leave a data table naming logins
that no longer exist.
Leaving a workspace takes the tenant of the row it deletes rather than of the
cached identity, which a rename leaves stale, and group deletion takes the
settings row before the membership rows like every other path that frees a
principal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
* feat: add sso_groups_claim setting for login-time instance group sync
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YESxWqzt959S6TY6vbc4eG
* chore: bump ee-repo-ref for the SSO groups claim reconcile
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YESxWqzt959S6TY6vbc4eG
* chore: update ee-repo-ref to 3b89bfc11314a326a191101cfe3ef65f6f7f82a8
This commit updates the EE repository reference after PR #774 was merged in windmill-ee-private.
Previous ee-repo-ref: e388527f9adbbe466fe050ca8d1d236ce3342bc3
New ee-repo-ref: 3b89bfc11314a326a191101cfe3ef65f6f7f82a8
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Removing a member takes both the workspace settings row (to drop the tenants
naming them) and their `usr` row. Two of those paths running at once took them
in opposite orders and deadlocked; every path now takes the settings row first.
The role picker in the data table explorer is the design system's badge rather
than a hand-rolled button.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
The cleanup is one helper called from five routes, and the bug each time was a
route that did not call it. This drives four of them — a group deletion, a
folder deletion, a member leaving, a superadmin deleting the account — against
a real API server and asserts the role stops naming them, while the `*`
wildcard, which no deletion can free, is left alone.
Three deletions bypassed the workspace-user cleanup and left `u/<username>` on
the roles it named: the superadmin's global delete, which frees the name in
every workspace at once, and both leave routes. A member invited into one of
those names afterwards inherited the roles.
The settings lock says what it hands back — the config as stored, generated
role passwords included — so it carries `_unchecked` and the contract that goes
with it: callers authorize the read, and redact before passing the value on.
The role save took an advisory lock of its own while principal cleanup took the
settings row, so the two never excluded each other: a save could persist a
permissions block it had computed before a group's deletion took that group off
it. The settings form had the same shape with no lock at all — it carries the
old permissions forward by construction.
They all take the settings row now, before reading and until they have written,
which is the lock the cleanups already used. One mechanism, so there is no
ordering to get wrong, and the advisory lock goes away with its cached query.
The tenant removal says what it does not do: it authorizes nothing, and the
rules differ per caller — a workspace admin for a user, the owner for a group
or folder, no identity for the system paths — so the name carries `_unchecked`
the way the resolution helpers next to it do.
* docs: teach agents to pass a resource as $res:<path> in run arguments
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjRARL7JA7xm772iJP4mJk
* docs: extend run-argument rule to in-editor chats, fix run-as wording
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjRARL7JA7xm772iJP4mJk
* docs: tighten resource run-argument rule after review
- Drop the false rationale that "$var:" only works inside a resource value
from the write_variable description and its runtime rejection message; keep
the rule (a variable cannot reference itself).
- MCP resource-argument description: the title fallback renders "No title",
so say the title is only a label rather than that it can be empty. Guard the
real-newline fix with asserts in the existing enrichment test.
- Eval: assert the full "$res:f/evals/global/github_main" value as one prefix
so a wrong path with a right prefix fails.
- resources.md: narrow "a trigger's payload" to its configured static args.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjRARL7JA7xm772iJP4mJk
* docs: scope the run-argument rule to global chat, add an exact eval matcher
The ai_evals A/B on the two in-editor modes showed no effect: script mode
sonnet 5/5 both with and without the description, flow mode sonnet 5/5 and
haiku 5/5 on the baseline alone. A flow's input schema already carries
`format: resource-<type>`, so those modes have a signal global mode does not
give. Revert both files to keep the tool schemas free of a description that
buys nothing per iteration; global mode keeps it, where haiku goes 0/5 -> 5/5.
Add `stringEqualsAnyOf` to toolCallArgs and use it for the resource reference:
nothing in the eval resolves the value, so a prefix match accepted a near-miss
path like `$res:f/evals/global/github_main_backup`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjRARL7JA7xm772iJP4mJk
* docs: address cubic review — CLI wording, mock resource getter
- `-d --data` help on all four run/preview commands: give $res: and $var:
their own clauses instead of a parenthetical that read as if a resource
were a kind of variable.
- Mock backend: `getBenchmarkResource` now resolves AI-provider seeds as well
as plain ones, so it agrees with `existsResource` and `listResource` — both
report either kind, and a case that listed a resource and then read it by
path got a row it could not fetch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjRARL7JA7xm772iJP4mJk
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: let operators use wmill.datatable() from within running jobs
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHR4fytgt6m4q37WCXs2Rp
* fix: refuse content-driven redirects and deferral in the operator datatable exemption
* fix: check the datatable exemption against the expanded query, not the raw content
* fix: fail closed on a language-overriding expansion and state the exemption's real scope
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* fix: record supplied script lock hashes so importers can skip relocking
Creating a script with a caller-supplied lock — a CLI push, a git-sync deploy,
any create carrying a lockfile — stored the lock on `script` but never wrote the
matching `lock_hash(workspace_id, path, hash_script(lock))` row. Only
worker-generated locks did.
`try_skip_relock` treats a missing hash for an imported script as changed, so no
importer of such a script could ever satisfy the skip predicate: every deploy of
it relocked every importer, forever.
The create transaction now records the hash for any lock it accepts, including
the empty one a codebase or a language with no lock generation carries — the
worker writes `hash_script("")` there, and a path going from a real lock to an
empty one has to stop matching what its importers recorded. Only a lock left to
a dependency job is skipped, because that job writes it.
A workspace clone now carries `lock_hash` too, without which every
dependency-map snapshot the clone later recorded held NULL and nothing in it
could ever skip. `dependency_map.imported_lockfile_hash` is deliberately not
copied: it records what an importer resolved against when it was last locked,
the clone runs READ COMMITTED, and a relock landing in the source between the
scripts being cloned and that statement would attach a hash the cloned
importer's lock was never resolved against — a hash older than the cloned
scripts costs one relock, a newer one skips a relock that was needed.
Lock generation is untouched, as is everything a relock does once it runs. The
only behavior that moves is which relocks are skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138oct9a6SLEZvFyCQgHRBx
* fix: narrow to the create-path lock hash
Drop the workspace-clone copy of lock_hash. It sits outside the reported
bug, and its double join over `script` can emit a path twice where two
versions are live, which the unique key on (workspace_id, path) then
rejects, failing the whole fork.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138oct9a6SLEZvFyCQgHRBx
* fix: restore the workspace-clone lock hash copy, guarded against fanout
A path can hold two live versions, and both joins match on path alone, so
the select can emit it four times against a primary key that admits one.
Every such row carries the single hash the path has, so ON CONFLICT DO
NOTHING settles it rather than aborting the fork.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138oct9a6SLEZvFyCQgHRBx
* fix: hash a clone's own locks rather than copying the source's rows
A source row is only as current as the last write to it, and a supplied
lock deployed before this was recorded leaves one naming a lock the path
no longer holds. Copying that into a fork hands an importer a hash it
never resolved against; hashing what the clone holds cannot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138oct9a6SLEZvFyCQgHRBx
* test: pin the lock hash written on a no-op push
Removing that write leaves the assertion with no row, which is the state
a script deployed before this shipped would stay in.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138oct9a6SLEZvFyCQgHRBx
* refactor: share one lock hash writer between the create and clone paths
Both wrote the same upsert with different SQL. The existing writers fold
theirs into the statement that writes the lock itself, which is what keeps
the two consistent; these two have nothing to fold it into, so they take a
shared one instead. The clone walks its pages by path rather than listing
them first, dropping a query with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138oct9a6SLEZvFyCQgHRBx
* fix: stream a clone's locks rather than reading them in pages
script.lock is unbounded, so a page of them is bounded only by how many
it holds. Hashing each as it arrives keeps one in memory at a time and
lets the clone site collapse to a single call.
Also states on both writers that they check no access to the workspace
they write, which their callers are the ones to have established.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138oct9a6SLEZvFyCQgHRBx
* fix: make the lock hash writer safe to repeat and free when unchanged
A path given twice in one call would have Postgres reject the whole
statement, so the last hash for each wins. And recording a hash a path
already has cut a row version for nothing on every unchanged sync, which
is the mode the no-op push runs in.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138oct9a6SLEZvFyCQgHRBx
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.
A linked variable owns the resource at its path: deleting one — singly or in
bulk — deletes it, and renaming one moves it, so all three answer to the rule
the resource endpoints do.
And a data table role's tenants are stored as u/<username>, which the executor
compares against the caller's name. A username rename that skipped them took
the role away from the user it followed and left it waiting for whoever took
the old name next.
Also: the ACL endpoints stopped saying 'admins only' — a non-admin may act on
what their role owns, which is the contract the handlers implement.
Comparing identities only answered for an edit that kept the resource. Clearing
its value, deleting it, renaming it away or bulk-deleting it all left the config
naming a database its roles were never created in — and the next resource at
that path would answer for grants it never had. Every one of those refuses now
while permissions are enabled, and the same disable-first escape stays.
create_resource with update_if_exists is an edit when the row is already
there, so it answered to none of the rule the other two write paths do. And
the lookup the guard makes had no offline entry, which is what CI compiles
against.
`get_public_settings` is the one the logged-in layout calls for every user, and
it served the data table config as stored — generated role logins included. A
member who is a tenant of no role could read every role's password and connect
as it directly, which is the whole tenant model. It goes through the same
redaction as the admin settings and the tarball now.
Also, on the way there:
- The permissions drawer opened from a data table's row sent the role the
manager is connected as, which belongs to another data table: roles are
per data table, so a row under another one is read as its own default role.
- The grant repair on an instance database ran before `apply_datatable_acl`
authorized anything, so any member could drive that privileged connection
with a request about to be refused.
- That repair also assumed schema `public` exists, and failed whole where it
had been dropped.
The path in the config staying the same said nothing: a postgres resource is
editable in place, so its host, database or user could change underneath roles
whose logins and grants live in the database it used to name. The identity a
connection resolves to is what has to hold still while those roles exist; a
password rotation is not an identity change and stays allowed.
Also generalizes the admin guard: what 'admin' holds is what every role here
connects through, on the database and on schema public alike, so a revoke
naming it is refused wherever it is aimed.
* fix: tolerate string app_id in GHES app config deserialization
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Rh73nHumzCbyd4Gwf6kw6
* fix: address review — strict app_id validation, drop dead variant
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Rh73nHumzCbyd4Gwf6kw6
* chore: update ee-repo-ref to b52c6471d517d979a9887f207a36347b1af376c8
This commit updates the EE repository reference after PR #767 was merged in windmill-ee-private.
Previous ee-repo-ref: ab2dc653719f9d65eb10964d1e2b5bc1b94d6535
New ee-repo-ref: b52c6471d517d979a9887f207a36347b1af376c8
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix: portal the confirmation modal so drawers cannot cover it
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* fix: log a folder acl grant under the permission it granted
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* fix: keep a table's actions column at its right edge
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* feat: edit a folder in a drawer that saves once
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* refactor: call the people on a folder or item members
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* fix: edit a folder against the workspace the drawer targets
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* refactor: drop the now-unused sticky actions column
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* docs: correct the script editor drawer's modal placement note
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* fix: pin the actions column without losing the row's hover tint
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* feat: show the pinned column's seam only while the table overflows
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* fix: draw the pinned column's seam as a shadow so it does not scroll away
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* fix: fade the pinned column's tint in step with its row
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* refactor: address review nits on the folder editor and pinned cell
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* fix: keep the folder draft across a user-store refresh
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* refactor: extract and test the folder draft's dirty check and permission diff
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* fix: stop the folder editor showing state the server refused
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* fix: keep a folder draft that no request ever reached the server
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9UCPLT4t8PmrWunjfFsPW
* fix: keep unapplied folder edits dirty when a save partially fails
* fix: block folder form edits while a save is in flight
* fix: commit a typed folder label before save snapshots the draft
* fix: count a typed folder label as an unsaved change
* fix: keep escape in the label input from closing what encloses it
* fix: capitalize folder table headers and drop a dead portal target
* refactor: make the confirmation modal portal opt-in per call site
* docs: name the stacking context that actually traps the discard dialog
* fix: report a half-landed member removal so the baseline reconciles
* feat: edit a group in a drawer that saves once
* fix: freeze the group name once the group exists
* fix: revoke the caller's own group acl last so the rest of the save is authorized
* docs: state the group call-ordering invariant once
* fix: report a failing post-save reload instead of dropping the rejection
* fix: hand the folder list reload back so a failure is reported
* fix: treat a rejected group create as inconclusive and catch a throwing onSaved
* revert: stop inferring a group was created from its name being taken
* fix: say when a failed group create may have saved the group anyway
* fix: key the may-have-been-created hint on the name conflict, not the status
* fix: skip the may-have-been-created hint when the group is known to exist
* feat: open a folder's group member from its row
* fix: stop showing the caller as an admin when the read failed
* fix: give up the caller's own folder admin last, and label a create as one
* fix: drop a folder member's acl before its owner entry
* fix: remove a folder owner before their acl, and correct the rls rationale
* docs: say the refusal is on the caller's last admin handle
* fix: defer only the folder rows the caller is an admin through
* docs: describe callerOwners as what the caller passes in
* docs: drop the call-site restatement of the diff's own invariant
* docs: record manager as a legacy group role
* fix: treat a sent request as possibly committed when reconciling
* fix: reconcile on any failed edit, and compare members as a set
* fix: keep write access when only the reconcile read fails
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Its roles live in the database it points at: the logins were created there and
every grant they hold is recorded there. Carried onto another database they
authenticate against a cluster that never heard of those grants. Opting out
first is what drops them from the database they belong to.
Also: the ATTACH test now calls the parser the executor runs, rather than a
byte-identical copy of its regex that no regression could reach.
admin is the login the data table reaches Postgres through, so revoking on
the database itself takes away what every role here connects with — and what
the role that would grant it back connects with. Refuse it server-side, and
stop the drawer from offering a row it cannot act on.
The ownership guard took the named-objects branch whatever the scope, and
returned. A revoke naming one table the caller owns, scoped to all tables,
therefore passed a check over that one table and planned a statement that
names the whole schema — the objects never reach the SQL there.
Owning the target is not owning what a change through it covers: a scope that
reads IN SCHEMA names every object in the schema, and handing a schema over
takes them all with it. Postgres would have skipped the ones the caller does
not own — these statements run as the data table's admin, so it will not.
Refuse, naming the object that is not theirs, and let a workspace admin
through as before.
A default-privilege rule speaks for the role that creates the objects, so a
non-admin now only writes them for the roles they may run as.
Also: the revoke button follows can_manage like the grant builder already did,
the copy path resolves a data table as admin for an admin (dumping as a
restricted role silently omits what it cannot read), and two doc comments now
sit on the function they describe.
* feat(ai-chat): make reusable skills ai_skill resources you select per workspace
* chore: pin the ee ref to the skill telemetry counters
* fix: address review findings on skill authoring, import and migration
* fix: enforce skill selection in read_skill and stop imports clobbering resources
* feat: carry format_extension from the hub into synced resource types
* fix: let an edit set or clear a resource type's format_extension
* fix: regenerate the sqlx cache and close the review round findings
* fix: close the round-2 findings on folder ACLs, cached sync and truncation
* refactor: make the skills migration non-destructive and use design-system inputs
* fix: close the round-4 findings on folder owners, startup sync and truncation
* fix: clear obsolete extensions, guard folder owners, and report skipped skills
* fix: honor explicit-null extensions and report same-type migration conflicts
* fix: scope skill actions to the committed workspace and paginate the listing
* fix: keep the drawer scoped to the live workspace and surface truncation
* fix: discard a skills refresh for a workspace the chat has left
* chore: update ee-repo-ref to 6efe7a73c745c2e1377a34498523c00d89010a3d
This commit updates the EE repository reference after PR #764 was merged in windmill-ee-private.
Previous ee-repo-ref: 55998c142bc72edd08532748af1974b16035658d
New ee-repo-ref: 6efe7a73c745c2e1377a34498523c00d89010a3d
Automated by sync-ee-ref workflow.
---------
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
A revoke's objects came from the request, argument types included, and those go
into the statement unquoted — so a schema owner could close a routine signature
and append SQL that ran as the data table's administrative login. The request
now only names an object: what reaches the statement is read back from the
catalog, and an object that resolves to nothing is refused.
The planners were behind `private`, which community builds carry, so the
permissions API answered on a CE binary. They take `enterprise` as well, with
a test that pins the refusal in every other edition.
Default privileges are read back scoped to one data table's own roles: two data
tables can share a database, and a new role of one was inheriting the other's
rules.
A role with a stored pg_rolename but no password resolved to the data table's
own connection, which owns everything — so a caller authorized as one role got
the admin one instead. Exports and git-synced settings redact that password, so
a restored config is exactly the shape that produced it. Refuse instead, and
name the fix.
The ACL endpoints took any workspace member: on a data table without roles
every member resolves to that same admin connection, so ownership and grants
there are the workspace admins' to change, as the roles themselves are.
Also: check a migration batch's roles before applying any of it, keep the role
picker for a single non-default role, drop the revoke button from a default
privilege on types (which no scope can express), and say what
get_datatable_resource_as_default_role actually resolves as.
Both planners now live in windmill-ee-private, reached through a dispatcher
that refuses in the open-source build, and an enterprise binary additionally
checks for an active license before planning anything. Reading a data table's
roles, its usable roles and a schema's owner and grants stays open, so an
instance that lapses can still see what it has; every mutation is refused.
The UI stops offering what the server would refuse: no Roles entry on a data
table, and no Permissions entry on a schema or table, without a license.
Functions live in pg_proc, so a schema changing hands left them behind and a
grant on ALL FUNCTIONS disappeared from the drawer on the next read, with no
way to revoke it. Read them alongside pg_class, and name them by their
identity arguments, which is what tells two of one name apart.
ALTER DEFAULT PRIVILEGES binds only the roles it names, so a role created
after a 'created later' grant produced tables no one else could read.
Replay the rules already in force for each new role.
The audit parameter of a data table config save carried the whole settings
blob, generated role passwords included. Redact it the way every other
export of that blob already is.
Both SDKs pasted the caller's role straight into the `-- role` annotation,
where a newline ends the comment and leaves the rest running as whatever the
first line named. Check the value against the role-name grammar the server
enforces.