diff --git a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts index f6374cb7c3..552ff87f5b 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts +++ b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts @@ -647,12 +647,14 @@ export class AIChatManager { scriptEditorGetLintErrors = $state<(() => ScriptLintResult) | undefined>(undefined) flowAiChatHelpers = $state(undefined) appAiChatHelpers = $state(undefined) - /** Datatable creation policy: enabled flag, datatable name, and optional schema */ + /** Datatable creation policy: enabled flag, datatable name, optional schema, + * and the role the app's queries run as. */ datatableCreationPolicy = $state<{ enabled: boolean datatable: string | undefined schema: string | undefined - }>({ enabled: false, datatable: undefined, schema: undefined }) + role?: string | undefined + }>({ enabled: false, datatable: undefined, schema: undefined, role: undefined }) pendingNewCode = $state(undefined) apiTools = $state[]>([]) aiChatInput = $state(null) diff --git a/frontend/src/lib/components/copilot/chat/app/core.ts b/frontend/src/lib/components/copilot/chat/app/core.ts index 5ab271cc6f..11c2588868 100644 --- a/frontend/src/lib/components/copilot/chat/app/core.ts +++ b/frontend/src/lib/components/copilot/chat/app/core.ts @@ -921,9 +921,14 @@ export function prepareAppSystemMessage(customPrompt?: string): ChatCompletionSy const policy = aiChatManager.datatableCreationPolicy const datatableName = policy.datatable ?? 'main' const schemaPrefix = policy.schema ? `${policy.schema}.` : '' - // Use wmill.datatable() for 'main' (default), otherwise wmill.datatable('name') - const datatableCall = - datatableName === 'main' ? 'wmill.datatable()' : `wmill.datatable('${datatableName}')` + // `wmill.datatable()` for 'main' without a role — the defaults — and the name + // and role spelled out otherwise. A role names the privileges the app's own + // queries run with, so it has to be in the code the model writes. + const datatableCall = policy.role + ? `wmill.datatable('${datatableName}', '${policy.role}')` + : datatableName === 'main' + ? 'wmill.datatable()' + : `wmill.datatable('${datatableName}')` let content = `You are a helpful assistant that creates and edits apps on the Windmill platform. Apps are defined as a collection of files that contains both the frontend and the backend. diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte index ebacf5c862..2b8f3b464f 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte @@ -878,7 +878,8 @@ aiChatManager.datatableCreationPolicy = { enabled: data.datatable !== undefined, datatable: data.datatable, - schema: data.schema + schema: data.schema, + role: data.role } // Start auto-snapshot @@ -900,9 +901,14 @@ // Read the current policy from aiChatManager const policy = aiChatManager.datatableCreationPolicy // Only update if different to avoid infinite loops - if (data.datatable !== policy.datatable || data.schema !== policy.schema) { + if ( + data.datatable !== policy.datatable || + data.schema !== policy.schema || + data.role !== policy.role + ) { data.datatable = policy.datatable data.schema = policy.schema + data.role = policy.role } }) diff --git a/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte b/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte index fc8abd5d5c..05fc14e534 100644 --- a/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte @@ -22,7 +22,9 @@ import { type DataTableRef, type RawAppData, formatDataTableRef } from './dataTableRefUtils' import { createDatatablesResource, + createRolesResource, createSchemasResource, + rolesWorthPicking, toDatatableItems, toSchemaItems } from './datatableUtils.svelte' @@ -72,6 +74,25 @@ () => selectedDatatable, () => opWs ) + const roles = createRolesResource( + () => selectedDatatable, + () => opWs + ) + + let selectedRole = $state(undefined) + const availableRoles = $derived(roles.current.roles) + const showRolePicker = $derived(rolesWorthPicking(availableRoles)) + + // The picked role belongs to the data table it was picked on, and the one it + // defaults to is what the app gets without saying anything. + $effect(() => { + const available = roles.current.roles + if (selectedRole === undefined || !available.includes(selectedRole)) { + selectedRole = available.includes(roles.current.defaultRole) + ? roles.current.defaultRole + : available[0] + } + }) const availableDatatables = $derived(datatables.current) const availableSchemas = $derived(schemas.current) @@ -148,7 +169,8 @@ input: { type: 'database', resourceType: 'postgresql', - resourcePath: `datatable://${selectedDatatable}` + resourcePath: `datatable://${selectedDatatable}`, + role: showRolePicker ? selectedRole : undefined } }) await dbOps.onCreateSchema({ schema: newSchemaName }) @@ -164,9 +186,10 @@ ? { tables: formattedTables, datatable: selectedDatatable, - schema: effectiveSchema + schema: effectiveSchema, + role: showRolePicker ? selectedRole : undefined } - : { tables: formattedTables, datatable: undefined, schema: undefined } + : { tables: formattedTables, datatable: undefined, schema: undefined, role: undefined } const policy: Policy = { on_behalf_of: $userStore?.username.includes('@') @@ -269,6 +292,23 @@ class="w-40" /> + {#if showRolePicker} +
+ +