From 93590da9d8a189c079e76a5e24a82afac2cd6bac Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Fri, 4 Sep 2026 16:38:11 +0200 Subject: [PATCH] fix(datatables): a lookup that failed is not an answer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01S5arH3G2Sa1Qqm32veJQ1n --- .../raw_apps/RawAppTemplatePicker.svelte | 10 +++++++--- .../components/raw_apps/datatableUtils.svelte.ts | 8 +++++++- .../workspaceSettings/ForkDatatableSection.svelte | 14 +++++++++++--- system_prompts/auto-generated/prompts.ts | 2 +- .../auto-generated/sdks/datatable-python.md | 2 +- system_prompts/generate.py | 4 ++++ 6 files changed, 31 insertions(+), 9 deletions(-) diff --git a/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte b/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte index 8e6b481721..36da23ef7f 100644 --- a/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte @@ -141,10 +141,14 @@ roles.current.permissioned && loadedRoles.length === 0 ) + // A lookup that failed is not an answer that there is nothing to pick, and an + // app naming this data table would be created against whatever the default + // turns out to be. + const rolesUnknown = $derived(rolesSettled && roles.current.failed) // Only the app that will name the data table is refused: with table creation // off nothing saves it, and an app that does not touch it is not this caller's // problem to be stopped over. - const blockedByRole = $derived(noUsableRole && tableCreationEnabled) + const blockedByRole = $derived((noUsableRole || rolesUnknown) && tableCreationEnabled) const accessSettled = $derived( hasNoDatatables || (rolesSettled && @@ -361,9 +365,9 @@ size="sm" class="w-40" /> - {#if noUsableRole} + {#if noUsableRole || rolesUnknown} - no role you can use + {rolesUnknown ? 'could not read its roles' : 'no role you can use'} {/if} {#if showRolePicker} diff --git a/frontend/src/lib/components/raw_apps/datatableUtils.svelte.ts b/frontend/src/lib/components/raw_apps/datatableUtils.svelte.ts index 2fcb438c24..59245bbdf9 100644 --- a/frontend/src/lib/components/raw_apps/datatableUtils.svelte.ts +++ b/frontend/src/lib/components/raw_apps/datatableUtils.svelte.ts @@ -57,12 +57,17 @@ export function createRolesResource( * there is nothing to pick — which is not the same as a permissioned one * this caller may run as nothing on. */ permissioned: boolean + /** The lookup itself failed, so neither of the above is an answer: an + * empty `roles` here means nothing was learned, not that there is + * nothing to pick. */ + failed: boolean roles: string[] defaultRole: string }> => { const empty = { datatable: datatableName || undefined, permissioned: false, + failed: false, roles: [], defaultRole: ADMIN_DATATABLE_ROLE } @@ -77,13 +82,14 @@ export function createRolesResource( } } catch (e) { console.error('Failed to load datatable roles:', e) - return empty + return { ...empty, failed: true } } }, { initialValue: { datatable: undefined, permissioned: false, + failed: false, roles: [], defaultRole: ADMIN_DATATABLE_ROLE } diff --git a/frontend/src/lib/components/workspaceSettings/ForkDatatableSection.svelte b/frontend/src/lib/components/workspaceSettings/ForkDatatableSection.svelte index 2ee61ad08b..8daa78ff15 100644 --- a/frontend/src/lib/components/workspaceSettings/ForkDatatableSection.svelte +++ b/frontend/src/lib/components/workspaceSettings/ForkDatatableSection.svelte @@ -58,10 +58,13 @@ workspace: ws, datatableName: dt.name }) - return { ...dt, permissioned: roles.enabled } + return { ...dt, permissioned: roles.enabled as boolean | undefined } } catch (e) { + // Not `false`: what the fork does with this data table is decided by + // the config, and saying "kept" for one the backend will drop loses + // it silently. console.error('Failed to read datatable permissions:', e) - return { ...dt, permissioned: false } + return { ...dt, permissioned: undefined } } }) ) @@ -213,7 +216,12 @@ items={[ { value: 'keep_original', - label: dt.permissioned ? 'Not shared (permissions enabled)' : 'Keep original' + label: + dt.permissioned === undefined + ? 'Keep original (permissions unknown)' + : dt.permissioned + ? 'Not shared (permissions enabled)' + : 'Keep original' }, { value: 'schema_only', label: 'Clone schema only' }, ...(!isCloudHosted() && $userStore?.is_admin diff --git a/system_prompts/auto-generated/prompts.ts b/system_prompts/auto-generated/prompts.ts index 262ecda2be..809e306f68 100644 --- a/system_prompts/auto-generated/prompts.ts +++ b/system_prompts/auto-generated/prompts.ts @@ -3052,7 +3052,7 @@ Import: \`import wmill\` # # Example: # wmill.datatable("main", role="operator") -def datatable(name: str = 'main', role: Optional[str] = None) -> DataTableClient +def datatable(name: str = 'main', *, role: Optional[str] = None) -> DataTableClient # Client for executing SQL queries against Windmill DataTables. class DataTableClient: diff --git a/system_prompts/auto-generated/sdks/datatable-python.md b/system_prompts/auto-generated/sdks/datatable-python.md index 2970d6d3b9..d227378fde 100644 --- a/system_prompts/auto-generated/sdks/datatable-python.md +++ b/system_prompts/auto-generated/sdks/datatable-python.md @@ -14,7 +14,7 @@ Import: `import wmill` # # Example: # wmill.datatable("main", role="operator") -def datatable(name: str = 'main', role: Optional[str] = None) -> DataTableClient +def datatable(name: str = 'main', *, role: Optional[str] = None) -> DataTableClient # Client for executing SQL queries against Windmill DataTables. class DataTableClient: diff --git a/system_prompts/generate.py b/system_prompts/generate.py index 6d99be1036..9ced329f65 100644 --- a/system_prompts/generate.py +++ b/system_prompts/generate.py @@ -1265,6 +1265,10 @@ def _format_py_params(node: ast.FunctionDef, skip_self: bool = False) -> str: if args.vararg.annotation: vararg_str += f": {ast.unparse(args.vararg.annotation)}" params.append(vararg_str) + elif args.kwonlyargs: + # The bare separator is part of the signature: without it the advertised + # call is positional, and an agent following it gets a TypeError. + params.append('*') for i, arg in enumerate(args.kwonlyargs): param_str = arg.arg