Commit Graph
14719 Commits
Author SHA1 Message Date
Diego ImbertandClaude Opus 5 0faedba495 feat(datatables): a shared data table is listed in the settings, read only
A fork could use its parent's data table but had nowhere to see that it
had one: the settings form reads the config, and the pointer lives outside
`datatables`. It is now a row of its own, naming the workspace it belongs
to, with no name to edit, no database to repoint and no delete — it is
configured, and its permissions administered, where it lives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 23:01:06 +02:00
Diego ImbertandClaude Opus 5 497f24bca7 feat(datatables): a fork shares its parent's data table, restrictions and all
A permissioned data table used to be dropped from a fork's config outright:
copying it would either hand every fork member the connection that owns the
parent's database, or freeze the parent's tenant lists at the moment of the
fork. So a fork could not use one at all.

It now gets a pointer instead — `shared_datatables: {"<name>": {"from":
"<workspace>"}}`, deliberately outside `datatables`, so nothing that plans
role drops or reads which logins a workspace claims can see it. Resolution
follows the pointer and asks every question of the workspace that owns it:
its live config, its live roles, and the caller as *that* workspace knows
them. Checking the fork's own identity would have made forking the way to
reach a data table as admin, since `can_use_datatable_role` starts at
`is_admin` and a fork's owner is an admin of their own fork.

A member of the fork who is not a member of the owner is refused, and does
not see the data table at all. A job permissioned as a folder or a group is
refused too: those exist per workspace, so the fork's admin creates both
sides of the name.

Administering it — permissions, ACLs, migrations, role drops — is refused
from the fork and belongs to the workspace that owns it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 22:42:29 +02:00
Diego ImbertandClaude Opus 5 c6c26fbd92 fix(datatables): a permissioned data table cannot be cloned into a fork
A clone lands in a brand-new database where none of the roles exist, and
the fork's copy of the config is stripped of its permissions — so every
member of the fork, including one whose role reached two tables, resolved
to the copy's own owner connection and read the whole thing. The fork also
inherits every member of the parent, so it took one admin clicking "clone
schema and data".

Refused at the two clone endpoints and at the fork wiring itself, which is
the one a caller cannot go around. A plain database-to-database import is
untouched: it lands nowhere that strips permissions.

Sharing the original is the supported answer, and carrying the parent's
restrictions into a shared data table is the next commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 22:06:04 +02:00
Diego ImbertandClaude Opus 5 1e6d28def2 fix(datatables): a data table that keeps its database keeps its roles
The fork-database drop cleared the permissions block and destroyed the
logins above the guards that refuse the drop, so a refusal — or a DROP
DATABASE that fails on an open session — left a live data table
unpermissioned, and every member of the workspace resolving to the
connection that owns it. The clear now happens only once the drop is
under way, and is put back if the drop does not happen.

The workspace-deletion snapshot takes the settings row first, so a
permissions save cannot add a login between the read and the row's
deletion.

The raw-app data drawer waits for the caller's usable roles before
mounting the content, like its sibling in the DB manager: mounting is
what fires the schema and metadata queries, and a first round sent
without a role runs — and caches — as whatever the server defaults to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 20:08:14 +02:00
Diego ImbertandClaude Opus 5 95c3059266 fix(datatables): what disappeared is read from the config, and its logins go first
The save planned drops from the request's `deleted_datatables`, so a config that
drops one name and adds another — no rename declared, no deletion declared —
left the first one's logins for whatever data table is created under that name
next. What disappeared is the difference between the two configs.

Dropping a fork's data table databases dropped them before anything asked about
their logins, which are cluster-wide and outlive the database. The permissions
block is cleared first, both because a data table whose database is going has no
business claiming roles in it and because that is what the drop reads to know
they are nobody's.

The raw app's tree is asked for as the role the drawer is using, like its
sibling: it was always the default role's view, under a picker that changes the
role.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 19:08:00 +02:00
Diego ImbertandClaude Opus 5 ec19b0fe21 fix(datatables): a generated login belongs to one data table in one workspace
Three ways it could stop being true, all of them ending with a login shared
between two data tables or outliving every record of itself:

A rename carried the roles verbatim, and their names are made from the data
table's name — so a data table created under the name that was left behind
generated those same names, adopted those logins and reset their passwords.
Renaming a permissioned data table is refused, the way pointing one at another
database already is.

Deleting a workspace removed the settings that name its logins without dropping
them. What to drop is resolved while that row is still there and run after the
commit, like a data table's own deletion — a workspace id is reusable, so what
survived was adoptable by the next workspace of that name.

Generating the initial migration takes a `pg_dump` with the data table's own
connection and returns every object in it, on nothing but `ApiAuthed` — a member
who may run as no role could read the whole schema through it. It answers to the
gate the rest of the migration management does.

The raw app's data drawer asks for its tree when it opens, like the manager's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 18:50:00 +02:00
Diego ImbertandClaude Opus 5 443d8a5316 fix(datatables): the manager reaches every database only when it is open
This PR swapped the drawer's data table list for one that reports each table's
schemas and privileges, which connects to every configured database in turn —
and the drawer is mounted on every logged-in page, so that ran on page loads
with the manager closed, up to a connection timeout per unreachable database.
It is asked for when the drawer is open.

The raw app's data drawer keeps a role across sessions while both entry points
replace the data table under it, so browsing one table as `analyst` and then
opening another queried that one as its own `analyst`, or as a role it does not
have. Both clear it, like the in-drawer switch already did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 18:08:23 +02:00
Diego ImbertandClaude Opus 5 3de44a8f8d test(datatables): pin which resolutions prove the database, not the comparison
The test added with the last commit called the comparison directly, and that
never took a role — moving the call back inside the branch that swaps credentials
would leave it passing. The rule is a function now, and asserting it fails when
the rule is reverted: an identified caller of a permissioned data table proves it
whatever role it lands on, the machinery that repairs one does not, an
unpermissioned one has nothing to prove.

The exemption's comment names what `Unchecked` actually covers. Two of its
callers are not repair: the admin diagnostic, and the postgres trigger's
replication connection — which reaches the data table's own connection the way
every caller did before roles existed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 17:51:46 +02:00
Diego ImbertandClaude Opus 5 9580e9985a fix(datatables): admin follows the same record as every other role
`admin` has no login to swap in — it is the data table's own connection — so the
check that the data table still points where its roles live sat inside the branch
that swaps credentials and never ran for it. A caller granted `admin` was handed
whatever the resource pointed at, which is the case the record exists for: the
guards on the resource can be gone around through a `$var:` or a concurrent
enable. It runs for every permissioned data table now, whatever role the caller
lands on, and only for callers with an identity — the machinery that repairs a
moved data table resolves as admin and has to keep working, which is what
re-saving its permissions does.

The disabled schema toggle's tooltip names the specific refusal first, like the
hint beside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 17:40:30 +02:00
Diego ImbertandClaude Opus 5 1b82a7c236 fix(datatables): the specific refusal is the one worth saying
With no usable role the access question goes out role-less, which the server
answers as the data table's default — the role that was refused — so the entry
comes back with an error and both refusals are true at once. The one that names
the cause is tested first.

`_format_py_params` is now what its comment claimed: positional-only parameters
keep their `/`, their defaults line up with the positional ones, and the second
renderer is gone rather than kept one clause behind. Every generated file comes
out byte-identical, since nothing in the SDK uses `/` yet — which is exactly how
the missing `*` survived.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 17:20:23 +02:00
Diego ImbertandClaude Opus 5 7cd6e081a7 refactor(prompts): one renderer for Python signatures
`extract_py_functions` inlined a third copy, one clause behind the two functions
that already do this — it lost the bare `*` until last commit, and still dropped
positional-only arguments and annotated varargs. It calls the shared one; the
generated files come out byte-identical.

The picker says which of the three refusals it hit, including the database it
could not reach, and an app that keeps no data table no longer creates a schema
in one: the app would never refer to it, and where the database is unreachable
the failure is one the user cannot act on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 17:09:35 +02:00
Diego ImbertandClaude Opus 5 8a40ff46fd fix(prompts): a keyword-only argument is advertised as one everywhere
The general Python extractor dropped the bare `*` the same way the datatable one
did, so every keyword-only signature it emits — `datatable`, `task` and the rest
— told an agent to pass those arguments positionally, which is a TypeError.
Regenerated: the script prompt, the Python skill and the CLI guidance carry the
separator now.

The fork dialog says both outcomes where the permission check did not answer:
what the backend does with the data table is decided by the config, so a label
promising the original is kept is a promise it cannot make.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 16:59:07 +02:00
Diego ImbertandClaude Opus 5 b1474820b5 fix(datatables): a database that could not be reached is not a role that refused
`can_create_schema: false` on an entry the backend kept with an error says
nothing about the role, but the modal read it as one: it left `New`, could not be
put back, and the tooltip blamed the role for a database it never reached. The
access answer carries its failure, the flip waits for an answer, the tooltip says
what actually happened, and an app that would name that data table waits with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 16:52:01 +02:00
Diego ImbertandClaude Opus 5 93590da9d8 fix(datatables): a lookup that failed is not an answer
An empty role list meant "nothing to pick" whether the endpoint said so or never
answered, so a failed lookup on a permissioned data table let an app be created
naming it with no role — the case the check exists for. The failure is carried,
and an app that would name the data table waits for an answer.

The fork dialog said "Keep original" on the same failure, for a data table the
backend drops from the fork when its permissions are on: it says the check did
not answer instead of asserting the outcome.

The generated Python signature keeps the bare `*`. `datatable(name, *, role)` is
keyword-only, and an agent following the advertised positional form gets a
TypeError.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 16:38:11 +02:00
Diego ImbertandClaude Opus 5 7c68818da1 fix(datatables): a role picked at creation is the role the app keeps
Leaving it out when it happened to equal the data table's default meant "whatever
the default is then", so an app created as `analyst` became a `reader` app the day
an admin changed that default. The pick is written down.

The hint says what is true of the choice in front of it and no more: what the
assistant writes when an app names no data table is a property of the prompt, not
of this data table.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 16:32:42 +02:00
Diego ImbertandClaude Opus 5 a253ba3d11 fix(datatables): the hint states the situation rather than a way out that is not one
Turning table creation off lets the app be created, but it does not stop the
assistant writing queries against the data table: with none named, what it writes
falls back to the workspace's default — the same one the caller may run as no
role on. The hint says what will happen instead of recommending an escape that
leads back to it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 16:21:34 +02:00
Diego ImbertandClaude Opus 5 6608180e3e fix(datatables): the default-database popover waits for the answer too
Its Schema select read the access resource without the stamp its sibling reads,
so switching the database one row above left the previous data table's schemas
listed and clickable — a schema name the new data table may not have, written
into the app's policy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 16:12:32 +02:00
Diego ImbertandClaude Opus 5 bdcd42ac0a fix(datatables): only the app that would name the data table is refused
A caller who may run as none of a permissioned data table's roles could not
start anything: the refusal ignored whether the app was going to name it at all.
With table creation off nothing saves the data table, and an app that never
touches it is not theirs to be stopped over — so the buttons look at that, and
the hint says what the way out is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 16:02:09 +02:00
Diego ImbertandClaude Opus 5 4638f35951 fix(datatables): a workspace with no data tables has nothing to wait for
The gate that keeps the modal from deciding before an answer arrives never
settles where there is no question: with no data tables, nothing is selected, so
both Start buttons stayed disabled under an alert saying an app can still be
created. Waiting is for a data table that was asked about.

A data table this caller may run as no role on is refused instead: leaving the
role out saves its configured default, which is the one they cannot use, so the
app would be created with queries that are refused. The picker says so where the
data table is chosen.

The six rounds of fixes around the loading state left three comment blocks two
paragraphs deep, one of them documenting the item below it. The rule they all
restate is written once, where a reader would break it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 15:53:30 +02:00
Diego ImbertandClaude Opus 5 1e2052d74a fix(datatables): undefined on both sides is not an answer
Before auto-select lands there is no data table to have asked about, and the
initial value stamps none either, so the gates added to keep the schema mode from
flipping against a non-answer compared `undefined` with `undefined` and let it
through at mount — the modal opened on `None` rather than the `New` it declares,
and an app started without touching the toggle got no schema. Settled now means a
data table is selected, the role list answers for it, and the access answer was
asked as the role the app will run as.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 15:31:19 +02:00
Diego ImbertandClaude Opus 5 3d2fa49358 fix(datatables): the role an app can use is the one it saves
Hiding the picker where there is one usable role meant saving none, and no role
means the data table's configured default — which is not filtered by what the
caller may use. A member whose only role is `admin` on a data table defaulting to
`analyst` got an app whose queries are refused. The role is saved whenever it is
not what the data table would resolve to anyway, and the schemas are asked for as
that role whether or not the picker is shown.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 15:21:10 +02:00
Diego ImbertandClaude Opus 5 2d8a99d2a3 fix(datatables): an answer about another role settles nothing
What a role may create in is the question the access resource asks, so an answer
computed as a different one is not an answer to it — and there is always one of
those: the role getter is empty until the role list settles, so every switch
fetches once with no role, which the server reads as the data table's configured
default. That answer stamped the data table alone, passed the check, and its
one-way flip took the modal off `New` for a role that can create schemas.

Both halves of the key are stamped and compared now, and Start waits for that
answer too: a new schema name checked against a list from another question is
not checked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 14:49:51 +02:00
Diego ImbertandClaude Opus 5 8868c5ff72 fix(datatables): nothing is decided before the answer for this data table arrives
Starting a new app while the role list loads saved no role, which the server
resolves to the data table's configured default — a role the caller may not be
allowed to use, where a moment later the picker would have chosen one they can.
Start waits for the list that answers for the data table selected.

The schemas and the right to create one get the same stamp the role list has. At
mount they are the initial value rather than an answer, and the effect read that
as "this role cannot create schemas", flipping the modal off `New` with nothing
to put it back; during a switch they are the previous data table's.

A role drop that fails names what it leaves behind whichever statement failed:
the config that stopped naming them has committed either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 14:41:11 +02:00
Diego ImbertandClaude Opus 5 1e3e08566d fix(datatables): a row action names the data table it was asked for
A pending action kept only what to do, so a switch that landed anywhere else —
the target failed to load, or the user moved on — ran it against whatever the
manager was connected to by then. Dropping a schema is not a thing to do by
approximation: it carries its data table and is discarded when that is not where
we are.

The new-app picker reads the role list only once it answers for the data table
selected. During a switch it offers nothing rather than the previous table's
roles, and an app created in that window gets the new table's default instead of
a name carried over.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 14:17:35 +02:00
Diego ImbertandClaude Opus 5 7f09781926 fix(datatables): the role list answers for the data table it was fetched for
Switching the new-app picker to another data table leaves the previous one's
roles in hand until the request lands, so resetting against them chose that
table's default and recorded the choice as this table's — and a role name the two
share survived the switch. The list now says which data table it answers for, and
the picker waits for the one it asked about.

The fork dialog says "Not shared (permissions enabled)" where it used to say
"Keep original", which for a permissioned data table now means it is absent from
the fork. The listing carries that state so it cannot pair with another
workspace's data tables.

A failed drop names only the roles it actually tried; the ones the config named
again were skipped, and the caller could not tell them apart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 13:41:28 +02:00
Diego ImbertandClaude Opus 5 e6ca0de9c3 fix(datatables): a fork does not carry a copy of who may run as what
A data table the fork shares rather than forks still points at the parent's
database, and its cloned permissions block froze the tenants at the moment of
the fork: the parent revoking one never reached the copy, so a member who forked
while authorized kept running as that role afterwards. Dropping the block instead
would hand the fork the data table's own connection, which owns everything — so a
permissioned data table is not shared into a fork at all.

A deferred drop that fails is the end of the line for those logins, since the
config that named them has committed and no later plan diffs against them. The
error says so, and names them.

The new-app picker resets its role when the data table changes rather than only
when the name is gone: two data tables can both define an `analyst`, and the name
surviving is not the role surviving.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 13:14:51 +02:00
Diego ImbertandClaude Opus 5 a274fd85bc fix(datatables): a role belongs to the data table it was chosen on
An app's default data table can be changed after it is created, and both editors
that change it kept the role picked for the previous one — so its queries named
a role that data table has never heard of. The rule is one function now, used by
both, and the selector lists schemas as the role rather than as whatever the
data table resolves to by default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 12:36:53 +02:00
Diego ImbertandClaude Opus 5 e4ce3a466a fix(datatables): order the offboarding that takes the locks, not the preview that reads
The ordering landed on `global_offboard_preview`, which takes no locks at all;
`offboard_global_user`, which takes each workspace's settings row, still walked
its memberships in whatever order the query returned.

`read_datatable` and `connect_as_admin` say what they hand out and what a caller
owes them: the first returns the config with its generated role passwords, the
second the connection that owns every object in the data table's database.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 12:11:25 +02:00
Diego ImbertandClaude Opus 5 2d09d33997 fix(datatables): the three transactions that still locked before the settings row
Offboarding reassigns a departing user's scripts, flows, apps and resources —
rows a rename writes while holding the settings row — so it takes that row
first, and offboarding from several workspaces walks them in `workspace_id`
order. Removing a member locked the `usr` row before the removal reached the
settings row, and a rename renamed the `password` row before its first
workspace: both now come after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n
2026-09-04 11:47:52 +02:00
Diego ImbertandClaude Opus 5 8bb2b0d777 fix(datatables): one lock, taken first, instead of an order per handler
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
2026-09-04 11:28:44 +02:00
Diego ImbertandClaude Opus 5 b58a036dd8 fix(datatables): nothing irreversible happens before the save that asks for it commits
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
2026-09-04 10:48:58 +02:00
Diego ImbertandClaude Opus 5 43d2c29988 fix(datatables): a rename reaches only the workspace it is renaming in
`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
2026-09-04 10:09:48 +02:00
Diego Imbert 42271dcf7f Merge remote-tracking branch 'origin/main' into datatable-perms-3
# Conflicts:
#	backend/ee-repo-ref.txt
2026-09-04 09:32:27 +02:00
Diego ImbertandClaude Opus 5 f3e3281336 fix(datatables): a deferred role drop stops at the data table that holds those roles now
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
2026-09-04 08:47:14 +02:00
0d6bce4a12 keep the SSO group reconciler alive in oauth2-less builds (#10969)
* 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>
2026-09-04 01:23:59 +02:00
Ruben FiszelandClaude Fable 5.1 11138284ac fix: deploy a relocked script version only when its lock changed (#10966)
* fix: deploy a relocked script version only when its lock changed

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdEb6gzCZ2qXmAQJAeMf9W

* fix: write the unchanged relock hash under the row lock and skip the phantom tally

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdEb6gzCZ2qXmAQJAeMf9W

* fix: requeue a superseded relock and read the live head past the script cache

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdEb6gzCZ2qXmAQJAeMf9W

* fix: re-read the relock head after waiting on its lock and keep module locks

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdEb6gzCZ2qXmAQJAeMf9W

* fix: bound the relock head re-read instead of reading once

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdEb6gzCZ2qXmAQJAeMf9W

* chore: refresh the sqlx cache entry for the re-indented lock write

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdEb6gzCZ2qXmAQJAeMf9W

* test: pin the waiting-relock requeue and the multi-file importer no-op

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdEb6gzCZ2qXmAQJAeMf9W

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 01:01:36 +02:00
Ruben FiszelandClaude Opus 5 6a7a6d9144 chore: stop denying reads of secret files in claude settings (#10968)
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.


Claude-Session: https://claude.ai/code/session_01RNCupPk2yewQT1JMNjkV8M

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 00:40:34 +02:00
Diego ImbertandClaude Opus 5 1f872c5edb fix(datatables): a role only works where it was created, and only under its own name
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
2026-09-03 23:59:04 +02:00
Diego ImbertandClaude Opus 5 3e3d2a6363 fix: keep braces inside string tool arguments out of JSON depth count (#10965)
Claude-Session: https://claude.ai/code/session_013vvU4UWCpib25ovmmAD7HH

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 22:30:19 +02:00
79426a1a68 feat: reconcile IdP instance groups from the SSO groups claim (#10957)
* 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>
2026-09-03 22:23:43 +02:00
Diego ImbertandClaude Opus 5 23c1fc470f fix(datatables): one lock order for every path that frees a username, one badge
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
2026-09-03 21:37:11 +02:00
Diego Imbert dfc06bf821 test(datatables): pin that freeing a principal takes its tenant with it
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.
2026-09-03 19:50:59 +02:00
Diego Imbert 2c55c3918c fix(datatables): every route that frees a username takes its tenants with it
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.
2026-09-03 19:43:39 +02:00
Ruben FiszelandClaude Fable 5.1 b100606da6 fix: patch critical CVEs in the worker image (#10962)
* fix: patch critical CVEs in the worker image (go, node, php, helm, libtiff)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TT3CyQED8iwwmsKMttk6PP

* ci: run the backend tests on node 24 to match the image

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TT3CyQED8iwwmsKMttk6PP

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 17:02:08 +00:00
Ruben Fiszelandrubenfiszel 38fc0d3a12 chore(main): release 1.803.0 (#10952)
* chore(main): release 1.803.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
v1.803.0
2026-09-03 13:08:33 +02:00
Diego Imbert 625753c2d4 fix(datatables): one lock for everything that reads the config and writes it back
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.
2026-09-03 12:17:26 +02:00
hugocasaandClaude Opus 5 e474e8803c feat: expose request headers to scripts invoked via MCP (#10903)
* feat: expose allowlisted request headers to scripts invoked via MCP

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4i1qCTY9HQMqCPTBeTiV9

* fix: close header-forgery routes flagged in review of MCP header passthrough

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4i1qCTY9HQMqCPTBeTiV9

* fix: match allowlisted headers exactly and withdraw every model-args run path

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4i1qCTY9HQMqCPTBeTiV9

* fix: address review nits on MCP header passthrough

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4i1qCTY9HQMqCPTBeTiV9

* fix: stop over-withdrawing deleteScriptByHash and align schema strip key space

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4i1qCTY9HQMqCPTBeTiV9

* refactor: move MCP header field detail into a label tooltip

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4i1qCTY9HQMqCPTBeTiV9

* fix: bound include_header parsing and narrow the duplicate-header drop

* feat: handle runnable-executing tools instead of withdrawing them

* docs: record the preprocessor kind seam on proxied run-by-path

* fix: strip every runnable argument map and open the field to gateway tokens

* fix: withhold connection credentials from runnables unless explicitly named

* fix: keep endpoint control arguments out of the transport-owned strip

* fix: exempt workspace_id from the strip only where it routes the call

* style: reindent the MCP header tooltip block

* refactor: deliver MCP request headers through the preprocessor only

* fix: widen the proxy-owned header set and clear docs left by the redesign

* fix: count proxied header delivery and finish the redesign doc sweep

* fix: forward proxied headers only to a runnable that has a preprocessor

* refactor: drop include_header and the MCP credential deny list

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4i1qCTY9HQMqCPTBeTiV9

* chore: restore the blank line in CreateToken

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4i1qCTY9HQMqCPTBeTiV9

* refactor: drop the mcp header_passthrough feature usage counter

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4i1qCTY9HQMqCPTBeTiV9

* test: pin that a caller credential other than the hop's own travels

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4i1qCTY9HQMqCPTBeTiV9

* refactor: deliver headers only through the direct script and flow tools

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4i1qCTY9HQMqCPTBeTiV9

* feat: withhold connection credentials and pin MCP header delivery end to end

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4i1qCTY9HQMqCPTBeTiV9

* test: send every credential the withheld-list assertions cover

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4i1qCTY9HQMqCPTBeTiV9

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 11:30:20 +02:00
GuilhemandClaude Opus 5 e39dd7eb12 docs: teach agents to pass a resource as $res:<path> in run arguments (#10927)
* 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>
2026-09-03 11:29:54 +02:00
GuilhemandClaude Opus 5 582761e37c feat: reuse an existing workspace resource in the project import wizard (#10935)
* feat: let the import wizard reuse an existing workspace resource

The project import wizard always opened the create-resource drawer, so a
workspace that already had, say, an SMTP resource still ended up with a second
one. Step 4 now offers a choice: fill in a new resource as before, or pick an
existing one of the same type.

Picking an existing resource rewrites the deployed items to point at it and
then deletes the imported stub. The rewrite covers scripts, flows, apps, raw
apps and every workspace trigger kind, and holds two rules: it writes nothing
unless every referrer can be rewritten, and it only touches items under the
target folder.

Raw apps re-upload the bundle shipped in the project export instead of
rebuilding it, and the retarget refuses when the deployed sources have moved on
since the import — that bundle was built from the export's sources, so
re-uploading it over edited sources would revert them.

Adds `update` to the trigger-kind table for the eleven kinds whose service
takes a plain config body; schedule keeps its own branch because
updateSchedule takes a different shape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj

* feat: only ask about resources the project actually points at

A project declares one resource per `resource-<type>` input schema as well as one
per `$res:` reference, so an app that pins `f/calendly/google_calendar` for a
script whose schema says `resource-gcal` ships an unreferenced `f/calendly/gcal`
alongside it. Step 4 listed both and asked you to fill in each.

Only the referenced ones have to hold a credential for the project to work. The
rest are still created — a standalone run picks from them in the argument picker
— but they no longer reach the checklist, and `resourceCount` counts the same
set so the wizard does not offer a fourth step that has nothing on it. Across
the twelve published hub projects this drops 9 of 19 rows, including three
non-credential input shapes in `typeform`.

Also fixes a miss in the retarget: a trigger holds its resource as a bare path in
its own `*_resource_path` field rather than as a `$res:` token, so a token-only
scan left it pointing at a stub that was then deleted. Detection now mirrors
`rewriteTriggerConfig` through a shared `referencesResourcePath`, which matches
the parsed structure rather than its serialization — keeping `f/proj/db` out of
`$res:f/proj/db_prod` as well.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj

* fix: refuse a resource retarget the scan or the rewriters cannot cover

Uncompiled trigger features 404 on their list route; that is the instance not
having the kind, not a listing that failed, so it no longer blocks every
retarget on a stock build. The `listSearch*` endpoints cap server-side with no
ordering and no pagination, so a full page is refused rather than read as the
whole workspace. An item that names the resource path outside a `$res:` token is
refused at plan time — no rewriter relocates it — and the trigger row keeps its
own `script_path` so a runnable sharing the path is not repointed. A raw app
whose sources the export cannot yield carries no entry at all, so the refusal
its comment promises actually fires. The reused row offers text instead of a
button that leads to a deleted resource.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj

* refactor: let an incomplete scan keep the stub instead of refusing the retarget

The scan behind "nothing is written unless every referrer can be rewritten"
cannot be proven complete: the listings come back capped, a trigger kind can
fail to list, and a reference can sit where no rewriter reaches. Gating the
whole run on that claim made every such case a refusal.

Rewriting an item onto the chosen resource is safe on its own — the item
resolves whether or not the stub survives — so only the delete needs the claim.
`planRetarget` now answers with the referrers it can move plus the gaps it
cannot account for, `applyRetarget` always moves the first set, and a gap keeps
the stub rather than stopping the run. A referrer outside the project's folder
is one of those gaps: the listings are workspace-wide, so it is seen for free,
it stays the user's own, and its existence is why the stub stays.

The outcome carries what moved and why the stub was kept, so the row settles to
the chosen resource either way and says when the placeholder is still there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj

* fix: preserve a retargeted item's deployed identity, and send back its own bundle

Every write here edits a deployed item in place, but none of them said so.
Without `preserve_on_behalf_of` the backend replaces the item's stored run
identity with whoever opened the wizard, and `updatePolicy(next, undefined)`
rebuilt an app's policy from nothing — dropping its sandbox rules and forcing
`execution_mode: publisher`, which puts a viewer app on the publisher's
identity even though the backend would otherwise have kept the deployed mode.
The policy is now recomputed from the deployed one, which is what the
triggerables rekeying actually needs.

The raw-app bundle no longer comes from the project export. The browser can
read a deployed bundle back — mint the app's public secret and fetch
`/apps/get_data/v/{secret}.{ext}`, the same route the Hub publish reads — so
the bundle sent back is the deployed one whoever last edited it. That removes
`ExportedAppFiles`, its plumbing through the setup step, `rawSourcesDiverged`,
and the two raw-app gaps: an app "edited since the import" is no longer a case
that exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj

* perf: carry the trigger row from the scan into its write

`rewriteTrigger` listed the whole kind again to find the row it had just read,
once per trigger — and for schedules a listing is itself a listing plus a
detail fetch per row. The scan already holds the row, so the referrer carries
it.

Pins two properties that nothing covered: the trigger update body leaves
`enabled` out, so pointing a trigger at a credential cannot also start it; and
a write that fails partway keeps the stub while reporting what had already
moved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj

* fix: keep unfilled resources out of the reuse chooser

The chooser offered every resource of the row's type except the ones this import
created, so a stub left behind by an earlier import of the same project showed up
as a credential to reuse. Pointing a project at another project's empty
placeholder is never the answer, and nothing downstream would have complained.

Candidates are now read back and the unfilled ones dropped, using the same test
the checklist uses to call one of the project's own resources blank. Past a cap
they are all offered rather than costing a request each: a workspace with that
many resources of the outstanding types is not the case this filters for.

Also drops the chooser's promise that the imported placeholder is removed. That
was true when the delete was unconditional; the stub is now kept whenever the
scan cannot account for everything, and the row says which happened once it has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj

* fix: move a retargeted item's bundle and identity, and see the paths it spells out

Four gaps between what the retarget claimed and what it did.

A trigger states its run identity as `permissioned_as`, not the `on_behalf_of`
the other kinds use, and the backend keeps the row's value only when
`preserve_permissioned_as` says so. Without the pair, a trigger created under a
folder's `default_permissioned_as` started running as whoever picked the
credential.

A raw app's bundle is compiled from its sources, so a `$res:` a source spells out
is baked into it. The import rewrites that copy — `retargetProjectExport` runs
while `/bundle.js` is still one of `files` — but the retarget fetched the
deployed bundle after that split and sent it back untouched, then deleted the
stub the app still read. The fetched bundle is now rewritten too, and a path it
names any other way keeps the stub instead.

A script's content is one string, so the whole-string match that finds a bare
path in a flow or an app could not see one written inside it. `getResource("f/…")`
was invisible to both the scan, which then deleted the stub under it, and the
step-4 filter, which dropped the row so nobody was asked to fill it.

Trigger listings cap at the server's DEFAULT_PER_PAGE, which this table does not
page past. A full page is now read the way a full `listSearch*` page is: as a
listing that cannot account for the rest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj

* fix: see a path a flow or app spells out, and name why an item did not move

The script scan was taught to see a resource path written inside code; flows and
apps were left on the whole-string test, which cannot. A flow whose inline module
runs `getResource("f/proj/db")`, or a raw app whose source does, was neither
rewritten nor recorded as a gap, so the stub was deleted while the deployed item
still read it. Reachable from the wizard, because the step-4 filter does see such
a reference and offers the row.

Both branches now use the same test as the script branch, and gap rather than
rewrite: the stub survives either way, so a `$res:` token in the same item still
resolves, and rewriting half an item would only make the plan and the write
disagree about what moved.

Each rewriter now says why it left an item alone instead of answering yes or no,
so a raw-app bundle that spells the path out is reported as a reference nothing
could move rather than as a concurrent edit.

Also corrects the resource-listing comment — `perPage` bounds the answer, the
route does not default to 30 — and asks the askable-resource question against the
export as published rather than the retargeted copy, so the step and the stepper
that decides whether to offer it give one answer. A path spelled out in code is
not retargeted, so only the raw export has its references and its resource paths
agreeing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj

* fix: a kept placeholder is still something to fill in

Reuse marked the row done and replaced its action with static text even when the
stub survived. A kept stub is empty and is still what every item the scan could
not move reads, so the step reported "You're all set" over a project running on a
placeholder, with no way back to filling it. Reachable from one hub project: a
raw app whose source spells the resource path out gaps everything, nothing is
rewritten, and the row went green anyway.

Such a row now stays outstanding, keeps its button, says which path items still
read, and re-checks on refresh so filling that placeholder in closes it.

Flows and apps also went back to being rewritten as well as gapped, matching what
the script branch already did — the reason given for skipping them was
contradicted by that branch, and a comment merely naming the path was enough to
strand an item's real `$res:` token on the stub.

Two things had to become precise for that to hold. What counts as rewritable is
now the presence of a `$res:` token rather than any reference, since a whole
string equal to the path is the unreachable case, not a movable one. And the
post-rewrite check reads tokens only: a path the item also spells out is the
plan's gap to record, and re-reading it at write time reported one item twice,
as both unmovable and changed underfoot. Writers now skip a write that would
change nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj

* fix: rewrite only the tokens, and let a filled placeholder close its row

The import's flow and app rewriters also remap a runnable's own path on an exact
match. That is right for the folder-wide map the import hands them, where every
path is moving. Here the map holds one entry, a resource path — and scripts,
flows and resources share a namespace, so a project shipping both a script and a
resource named `smtp` had the step calling it repointed at the credential.
Triggers were already guarded against exactly this; flows, apps and raw apps were
not. All three now rewrite the serialized value, which moves the tokens and
leaves every path alone.

A kept placeholder that the user then fills in now closes its row: `stubKept` is
cleared by the read that finds it filled, so the row stops saying items still
need it while showing a green check beside "You're all set".

A kept-stub row's button also goes straight to filling that placeholder rather
than reopening the chooser. A second retarget from there can only be a no-op —
every rewritable referrer is already off the stub — and it would have relabelled
the row after moving nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj

* fix: check staleness where it can be seen, and stop trusting a client-side licence

The post-rewrite check could no longer fail: since the rewrite became token-only
it ran over exactly what the check looked for, so it read as a guard while
guarding nothing. The staleness it named is real — the plan classifies items from
the search listings and each write re-reads its item by path — so the check now
happens on that fresh read, and looks for the spelling no rewrite reaches. A
referrer the plan already recorded as unreachable skips it: the stub survives
either way, and re-reporting the same item would say it was both unmovable and
changed underfoot.

Trigger kinds are no longer skipped by the client-side licence store. That store
is empty on an EE instance whose licence is unset or whose fetch failed, while
the rows are still in the database and the routes still answer — and a kind
skipped that way left no gap, so the stub went while an EE trigger still pointed
at it. On CE those routes are not registered and the 404 branch already says so,
from the server rather than from a store.

`askableResources` now pairs the export's resources with the retargeted ones by
position, the way `retargetProjectExport` maps them, instead of rebuilding the
path by slicing a prefix. An external path the bundle pulled in lands at
`f/<folder>/<name>` with a `_2` suffix on collision, which no slicing recovers —
and the row would have gone missing from a checklist the stepper still counted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj

* fix: a scan the caller is not shown all of cannot clear the stub for deletion

The listings the scan reads run as the caller, and row-level security filters
them inside the query. For anyone but a workspace admin that means an item they
cannot read is not absent from the answer so much as invisible in it: it does not
appear, and it does not count towards the full-page test that catches a truncated
listing either. A colleague's private script referencing the stub is exactly that
shape, so the scan reported a clean sweep and the stub was deleted out from under
it, with nothing said.

That is the one input to the completeness proof the destructive step rests on
that was never checked. A caller who is not shown the whole workspace now records
a gap like any other, so the rewrite still happens in full and the placeholder
stays.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj

* fix: ask whether this workspace's listings are complete, not a stale record's

`UserExt` is per-workspace and outlives a workspace change, which is why it
carries `workspace_id`. Reading `is_admin` off it without checking which
workspace it describes answers for the wrong one. Step 4 is reachable by reload —
it is built to be — and nothing on that path re-fetches the record, so it still
describes the workspace the user came from. An admin of their own workspace
importing into a shared one they are a plain member of got a clean scan over
row-level-security-filtered listings, and the stub was deleted under a referrer
they were never shown.

The question is now asked of the target workspace, through a predicate that can
be tested. An instance superadmin bypasses the policies everywhere, so that is
asked separately rather than read off the same stale record.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj

* style: format the wizard retarget files

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj

* fix: leave a trigger's runnable references alone, and read the app kind rather than guess it

A trigger's `on_failure`, `on_recovery`, `on_success` and `url` name a runnable,
and `rewriteTriggerConfig` remaps one on an exact match — right for the
folder-wide map the import hands it, wrong for a map holding a single resource
path. A schedule whose error handler ran a script sharing that path had the
handler pointed at the credential instead. The same reason `path` and
`script_path` were already restored; only the two prefixed shapes it remaps are,
so a field holding a `$res:` token still moves.

The scan guessed raw from low-code by looking for `files` and `runnables`,
because `list_search_apps` returns only the path and the value. Both writers
re-read the app anyway, and that record carries `raw_app`, so the write now
dispatches on it. A guess wrong in either direction was a deploy the backend
refuses for changing an app's kind, which aborted the run at that referrer.

Also drops the past-tense clauses from four test comments. Each already states
the invariant it guards; the rest described iterations of this branch that no
reader will have seen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj

* fix: restore a trigger's bare runnable references too

The prefixed spellings were put back after the rewrite; the bare ones were not.
`dynamic_skip`, `error_handler_path` and a websocket initial message's
`runnable_result.path` each hold a plain script path, which `rewriteTriggerConfig`
remaps on a whole-string match — so a trigger whose error handler ran a script
sharing the stub's path had that handler pointed at the credential.

All of them now come back from the row, taken from what `triggerHandlerRefs`
reads rather than enumerated by hand. A prefixed field is still restored only
when it holds the runnable spelling, so a `$res:` token in one still moves; a
bare field is a path and nothing else, so it is always restored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuzkt6NqsqzvSYSVKpR3pj

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 11:28:54 +02:00
Ruben FiszelandClaude Fable 5.1 ca8800959a fix: bump git sync hub scripts to cli 1.802.1, test the fork ui pull (#10955)
* fix: bump git sync hub scripts to cli 1.802.1, test the fork ui pull

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011yMLnAWdjpCEs5VyGMn9ww

* test: guard the ui pull preview shape and pin the pull script ids together

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011yMLnAWdjpCEs5VyGMn9ww

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 11:23:23 +02:00