From 468cf1d9941538d285264e8a8c57356b934ab25d Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 14 Oct 2026 15:06:26 +0200 Subject: [PATCH] feat: read and change workspace settings from ai sessions and mcp (#11741) * feat: read and change workspace settings from ai sessions and mcp Co-Authored-By: Claude Opus 5.5 * docs: cover wmill workspace settings commands in the settings guidance Co-Authored-By: Claude Opus 5.5 * docs: say unknown settings keys are warned and ee-only ones refused Co-Authored-By: Claude Opus 5.5 * docs: cover datatable_permissions and protection_rules in the settings guidance Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- backend/windmill-api/openapi.yaml | 9 + .../src/mcp/auto_generated_endpoints.rs | 40 +++++ cli/src/guidance/skills.gen.ts | 86 +++++++++ .../copilot/chat/ToolDiffCard.svelte | 23 ++- .../components/copilot/chat/global/core.ts | 16 +- .../chat/global/sessionToolset.test.ts | 9 + .../chat/global/workspaceSettingsTools.ts | 169 ++++++++++++++++++ frontend/src/lib/mcpEndpointTools.ts | 40 +++++ system_prompts/README.md | 1 + system_prompts/auto-generated/index.d.ts | 1 + system_prompts/auto-generated/index.ts | 7 + system_prompts/auto-generated/prompts.d.ts | 1 + system_prompts/auto-generated/prompts.ts | 60 +++++++ .../skills/workspace-settings/SKILL.md | 84 +++++++++ system_prompts/base/workspace-settings.md | 81 +++++++++ system_prompts/generate.py | 13 ++ 16 files changed, 634 insertions(+), 6 deletions(-) create mode 100644 frontend/src/lib/components/copilot/chat/global/workspaceSettingsTools.ts create mode 100644 system_prompts/auto-generated/skills/workspace-settings/SKILL.md create mode 100644 system_prompts/base/workspace-settings.md diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index e41bfc5013..d4d797e27e 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -5191,6 +5191,8 @@ paths: The workspace's settings in the layout of the settings.yaml that `wmill sync` writes. Admin only. operationId: getWorkspaceSettingsDocument + x-mcp-tool: true + x-mcp-tool-include-query-params: [] tags: - workspace parameters: @@ -5217,6 +5219,13 @@ paths: description: >- Applies a settings document, each setting through the endpoint that owns it. Admin only. operationId: applyWorkspaceSettingsDocument + x-mcp-tool: true + x-mcp-instructions: "Send only the top-level keys to change, each whole as getWorkspaceSettingsDocument returns it: a key the document omits is left as it is, and a key set to null clears it. Keep `__SENSITIVE_AND_UNCHANGED__` for a secret you were not given. Call with dry_run true first, show the user the changes and apply only once they agree." + x-mcp-tool-include-fields: + - document + - dry_run + x-mcp-tool-opaque-fields: + - document tags: - workspace parameters: diff --git a/backend/windmill-api/src/mcp/auto_generated_endpoints.rs b/backend/windmill-api/src/mcp/auto_generated_endpoints.rs index 73b1fd1792..f449902f97 100644 --- a/backend/windmill-api/src/mcp/auto_generated_endpoints.rs +++ b/backend/windmill-api/src/mcp/auto_generated_endpoints.rs @@ -56,6 +56,46 @@ pub fn all_tools() -> Vec { query_field_renames: None, body_field_renames: None, }, + EndpointTool { + name: Cow::Borrowed("getWorkspaceSettingsDocument"), + description: Cow::Borrowed("get the workspace settings document: The workspace's settings in the layout of the settings.yaml that `wmill sync` writes. Admin only"), + instructions: Cow::Borrowed(""), + path: Cow::Borrowed("/w/{workspace}/workspaces/settings_document"), + method: Cow::Borrowed("GET"), + path_params_schema: None, + query_params_schema: None, + body_schema: None, + query_field_renames: None, + body_field_renames: None, + }, + EndpointTool { + name: Cow::Borrowed("applyWorkspaceSettingsDocument"), + description: Cow::Borrowed("apply a workspace settings document: Applies a settings document, each setting through the endpoint that owns it. Admin only"), + instructions: Cow::Borrowed("Send only the top-level keys to change, each whole as getWorkspaceSettingsDocument returns it: a key the document omits is left as it is, and a key set to null clears it. Keep `__SENSITIVE_AND_UNCHANGED__` for a secret you were not given. Call with dry_run true first, show the user the changes and apply only once they agree."), + path: Cow::Borrowed("/w/{workspace}/workspaces/settings_document"), + method: Cow::Borrowed("POST"), + path_params_schema: None, + query_params_schema: None, + body_schema: Some(serde_json::json!({ + "type": "object", + "properties": { + "document": { + "type": "object", + "description": "The settings to apply, in the layout of the document. A secret given as `__SENSITIVE_AND_UNCHANGED__` keeps its stored value; one prefixed with `$encrypted:` is decrypted with the workspace key." + }, + "dry_run": { + "description": "Only compute the changes, without applying them.", + "type": "boolean" + } + }, + "required": [ + "document" + ], + "minProperties": 1 +})), + query_field_renames: None, + body_field_renames: None, + }, EndpointTool { name: Cow::Borrowed("listDataMetrics"), description: Cow::Borrowed("list declared measures and dimensions on DuckLake tables: Call this before writing any aggregate query over a DuckLake table. A declared measure is the canonical definition of that number, and reproducing it yourself will silently disagree with it (a `revenue` measure typically excludes refunds or test rows). Filter by `table` for one table's declarations, or by `path_prefix` (e.g. `f/analytics`) for everything declared under a folder; omit both to browse the whole catalog. Results are keyset-paged: a full page may mean more remain, so continue with the `cursor_*` params rather than assuming a measure does not exist. Use each returned `expr` verbatim, and when a measure has a `filter` write it as `expr FILTER (WHERE filter)` so measures with different predicates can share one GROUP BY. If a number you need has no declared measure, write your own aggregate as usual. Results are limited to declarations whose producing script the caller can read"), diff --git a/cli/src/guidance/skills.gen.ts b/cli/src/guidance/skills.gen.ts index e71d5e96fc..62e9c01013 100644 --- a/cli/src/guidance/skills.gen.ts +++ b/cli/src/guidance/skills.gen.ts @@ -35,6 +35,7 @@ export const SKILLS: SkillMetadata[] = [ { name: "write-workflow-as-code", description: "MUST use when writing or modifying Windmill Workflow-as-Code scripts using workflow, task, step, sleep, approvals, taskScript, taskFlow, task_script, or task_flow." }, { name: "write-pipeline", description: "MUST use when creating or modifying a data pipeline, a set of scripts marked `pipeline` and wired together by `on` / `materialize` annotations." }, { name: "datatables", description: "MUST use when changing a data table's schema, writing or running its migrations, or working with its roles and permissions." }, + { name: "workspace-settings", description: "MUST use when reading or changing workspace settings, or the settings.yaml file `wmill sync` keeps." }, { name: "cli-commands", description: "MUST use when using the CLI, including debugging job failures and inspecting run history via `wmill job`." }, { name: "preview", description: "MUST use when opening the Windmill dev page / visual preview of a flow, script, or app. Triggers on words like preview, open, navigate to, visualize, see the flow/app/script, and after writing a flow/script/app for visual verification." }, ]; @@ -8263,6 +8264,91 @@ A data table under roles is reached as a Postgres role, and Postgres enforces wh - a migration runs as the role its SQL declares on its first line, \`-- role \`, the same one in its \`down\`. One that declares none runs as \`admin\`, which only those who may use \`admin\` can do. Read the Permissions section of https://www.windmill.dev/docs/core_concepts/persistent_storage/data_tables before designing who reaches what: it covers tenants, grants, the default role and the roles a connection that names none ends up holding, rows that differ per viewer through row-level security, and how forks and raw apps behave under roles. +`, + "workspace-settings": `--- +name: workspace-settings +description: MUST use when reading or changing workspace settings, or the settings.yaml file \`wmill sync\` keeps. +--- + +# Workspace settings + +Everything an admin sets on the Workspace settings page is one document: a mapping of top-level +keys, the same layout as the \`settings.yaml\` file \`wmill sync\` keeps. Only a workspace admin can +read or change it. + +## Applying a document + +- A partial document changes only the keys it states; every key it omits is left as it is. +- Each key is replaced whole, so read its current value and send it back edited: a nested object + given with one field replaces the stored object with that one field. +- A key set to \`null\` clears that setting; an on/off switch cleared this way reads as \`false\`. +- Each setting is written through the same path the settings page uses, with the same validation + and audit log. The writes are not one transaction: when one fails the apply stops, and the + error says what was already written. +- A dry run reports the changes, as \`before\` / \`after\` per key, without writing anything. +- \`name\` is never applied: rename a workspace on the settings page. \`color\` is applied only when + set to a value, so \`null\` does not clear it. +- A key the server does not know is ignored and named in the warnings: check them, since a typo + otherwise reads as "nothing to change". +- A setting this edition does not offer (Enterprise only) is refused, by the dry run too. + +## Secrets + +\`slack_oauth_client_secret\` is the one secret. A read shows \`__SENSITIVE_AND_UNCHANGED__\` in place +of a set secret; sending that placeholder back keeps the stored value. A value starting with +\`$encrypted:\` is decrypted with the workspace key, and one encrypted with another workspace's key +is left unchanged with a warning; any other value is taken as plaintext. Never +write a secret you were not given: keep the placeholder. + +## Keys + +| Key | Value | +|---|---| +| \`auto_invite\` | \`{ enabled, mode: invite \\| add, operator, instance_groups?, instance_groups_roles? }\`. The instance groups are managed only when \`instance_groups\` is stated; clearing a group removes the members it added. | +| \`error_handler\` / \`success_handler\` | \`{ path, extra_args?, muted_on_cancel?, muted_on_user_path? }\`, \`path\` being \`script/\` or \`flow/\`. | +| \`error_handler_fallback_to_instance_alerts\` | Report failures to the instance critical alert channels when no error handler is set. | +| \`webhook\` | URL POSTed a JSON event on every change to the workspace's items. | +| \`ai_config\` | Providers, default and completion models, custom prompts, \`copilot_disabled\`. | +| \`large_file_storage\` | Workspace object storage: \`{ type, s3_resource_path \\| azure_blob_resource_path \\| gcs_resource_path, public_resource?, secondary_storage?, advanced_permissions? }\`. | +| \`git_sync\` | \`{ repositories: [...] }\`, each with \`git_repo_resource_path\`, \`script_path\` and \`settings\` (include/exclude paths and types). Server-owned fields (webhook secrets, sync state, credentials) are never read nor written. | +| \`datatable\` | \`{ datatables: { : { database: { resource_type, resource_path? } } } }\`. Role permissions are \`datatable_permissions\`. | +| \`datatable_permissions\` | \`{ : { default_role, roles: { : [tenants] } } }\`: who may connect to each data table as which role. Tenants are \`u/\`, \`g/\`, \`f/\` or \`*\`; \`admin\` is always a role, and \`default_role: null\` means no default. A data table the value omits is no longer under roles. Only a data table on a cluster Windmill manages can be. | +| \`protection_rules\` | \`{ : { rules: [...], bypass_groups: [...], bypass_users: [...] } }\`, rules among \`DisableDirectDeployment\`, \`DisableWorkspaceForking\`, \`RestrictDeployToDeployers\`, \`RestrictAnonymousAppDeployment\`, \`RestrictPublicRunSharing\`, \`RestrictGuestAppDeployment\`. A rule the value omits is deleted. The dev workspace lock is not listed and cannot be set here. | +| \`ducklake\` | \`{ ducklakes: { : { catalog, storage, ... } } }\`. | +| \`dbt_warehouses\` | dbt warehouse connections. | +| \`default_app\` | Path of the app shown as the workspace home. | +| \`default_scripts\` | \`{ order?, hidden?, default_script_content? }\` for the script language picker. | +| \`deploy_ui\` | \`{ include_path?, include_type? }\`: what the deploy-to-workspace UI offers. | +| \`operator_settings\` | What operators see and do: page visibility flags (\`runs\`, \`schedules\`, \`resources\`, \`variables\`, \`assets\`, \`audit_logs\`, \`groups\`, \`folders\`, \`workers\`, \`triggers\`) and write rights (\`manage_schedules\`, \`manage_triggers\`, \`builder_flows\`, \`builder_apps\`). A write right the value omits keeps its stored value. | +| \`mute_critical_alerts\` | Mute the workspace's critical alerts. | +| \`slack_team_id\`, \`slack_name\` | Read only: set by connecting Slack on the settings page. | +| \`slack_command_script\` / \`teams_command_script\` | Script or flow handling the Slack / Teams command. | +| \`slack_oauth_client_id\` / \`slack_oauth_client_secret\` | The workspace's own Slack OAuth app. Clearing either removes both. | +| \`public_app_execution_limit_per_minute\` | Rate limit for public apps, \`null\` for none. | +| \`guest_access_enabled\`, \`guest_jwt_public_key\`, \`guest_jwt_jwks_url\` | Guest access to public apps and the key that signs guest JWTs. | +| \`add_admins_and_developers_to_forks\` | Add the workspace's admins and developers to each fork. | +| \`color\` | Workspace color, \`#rrggbb\`. | + +## From the command line + +\`wmill workspace get-settings\` prints the document (\`--encrypted\` for secrets encrypted with the +workspace key, \`--json\` for JSON). \`wmill workspace apply-settings \` applies a partial +document from a YAML or JSON file: it shows the changes and asks before writing (\`--dry-run\` to +only show them, \`-y\` to skip the question, \`--replace\` to clear what the file omits). + +## In a synced repository + +With \`includeSettings: true\` in \`wmill.yaml\`, \`wmill sync pull\` writes the document to +\`settings.yaml\`, secrets encrypted with the workspace key (plaintext only with \`plainSecrets\`), and +\`wmill sync push\` applies it. Unlike a partial apply, a push treats the file as the whole +document: a setting the file omits is cleared, except \`operator_settings\`, \`color\`, the auto-invite +instance groups, and the keys \`ducklake\`, \`dbt_warehouses\`, \`deploy_ui\`, \`teams_command_script\`, +\`public_app_execution_limit_per_minute\`, \`guest_access_enabled\`, \`guest_jwt_public_key\`, +\`guest_jwt_jwks_url\`, \`error_handler_fallback_to_instance_alerts\` and +\`add_admins_and_developers_to_forks\`, which are managed only when the file states them. +\`datatable_permissions\` is pushed only with \`includeDatatablePermissions: true\` (or +\`--include-datatable-permissions\`). A push ignores \`protection_rules\`: \`protection-rules.yaml\` and +\`wmill protection-rules push\` sync them. `, "cli-commands": `--- name: cli-commands diff --git a/frontend/src/lib/components/copilot/chat/ToolDiffCard.svelte b/frontend/src/lib/components/copilot/chat/ToolDiffCard.svelte index 94e33374c3..eb0fc1194b 100644 --- a/frontend/src/lib/components/copilot/chat/ToolDiffCard.svelte +++ b/frontend/src/lib/components/copilot/chat/ToolDiffCard.svelte @@ -3,6 +3,8 @@ import ChatCollapsibleCard from './ChatCollapsibleCard.svelte' import ToolCodeDiffView from './ToolCodeDiffView.svelte' import ToolPreviewCard from './ToolPreviewCard.svelte' + import ToolConfirmationFooter from './ToolConfirmationFooter.svelte' + import { Check, XCircle } from 'lucide-svelte' import { diffIsWholeFile, diffLineCounts, @@ -21,8 +23,9 @@ const diff = $derived(toolCodeDiff(message)) const isStreaming = $derived(Boolean(message.isStreamingArguments)) const hasDiff = $derived(Boolean(diff && (diff.before !== '' || diff.after !== ''))) + const awaitingConfirmation = $derived(Boolean(message.isLoading && message.needsConfirmation)) // The card body is a bordered box: opened with nothing in it, it draws an empty line. - const hasBody = $derived(hasDiff || Boolean(message.error)) + const hasBody = $derived(hasDiff || Boolean(message.error) || awaitingConfirmation) // Keyed by call id: a bare flag would carry the expansion onto the next message that // reuses this instance. An active or failed call opens by itself, and remains open @@ -45,7 +48,11 @@ toggled = { id: toggleKey, open: true } } }) - const expanded = $derived(hasBody && (toggled?.id === toggleKey ? toggled.open : opensByDefault)) + // The Apply/Reject footer is inside the body, so a pending confirmation keeps it open. + const expanded = $derived( + awaitingConfirmation || + (hasBody && (toggled?.id === toggleKey ? toggled.open : opensByDefault)) + ) const lines = $derived(diff && expanded ? toolDiffLines(diff, isStreaming) : undefined) const counts = $derived( diff && expanded @@ -79,7 +86,7 @@ label={message.content} {expanded} onToggle={() => (toggled = { id: toggleKey, open: !expanded })} - toggleable={hasBody} + toggleable={hasBody && !awaitingConfirmation} shimmer={isRunning} class={message.isQueued && !message.error ? 'opacity-60 hover:opacity-100 transition-opacity' @@ -91,6 +98,16 @@ {#if diff && hasDiff && expanded} {/if} + {#if awaitingConfirmation} + + {/if} {#if message.error}
[] = [ // Every subject is authoring guidance written around the draft tools by name, and a // tool result is the one place the toolset filter cannot reach. const access = (ctx.helpers as GlobalToolHelpers | undefined)?.access + if (parsed.subject === 'workspace_settings' && access && !access.has('admin')) { + toolCallbacks.setToolStatus(toolId, { content: 'No workspace settings guidance' }) + return 'Only a workspace admin can read or change the workspace settings. Tell the user plainly that you cannot, and suggest asking a workspace admin.' + } const canAuthor = !access || access.has('write_draft') || @@ -4958,7 +4967,8 @@ export const globalTools: SessionTool<{}>[] = [ // Workspace DuckLake: pipeline storage prerequisite, and declared measures ...getDucklakeTools(), // Read-only tools over files the user attached to the conversation - ...fileTools + ...fileTools, + ...workspaceSettingsTools ] // Tools that only make sense inside an AI session (they drive the session's diff --git a/frontend/src/lib/components/copilot/chat/global/sessionToolset.test.ts b/frontend/src/lib/components/copilot/chat/global/sessionToolset.test.ts index ceb179779f..6389bd6ef3 100644 --- a/frontend/src/lib/components/copilot/chat/global/sessionToolset.test.ts +++ b/frontend/src/lib/components/copilot/chat/global/sessionToolset.test.ts @@ -197,6 +197,15 @@ describe('session tool policies', () => { expect((original.def.function.parameters as any).properties.type.enum).toContain('script') }) + it('offers the workspace settings tools to admins only', () => { + const settingsTools = ['get_workspace_settings', 'set_workspace_settings'] + for (const [, access] of REACHABLE_PROFILES) { + const names = shippedSessionTools(access).map((t) => t.def.function.name) + const offered = settingsTools.filter((n) => names.includes(n)) + expect(offered).toEqual(access.has('admin') ? settingsTools : []) + } + }) + // Naming a tool the model was not given produces invented calls. Asserted over the // ASSEMBLED message: the session-state, pipeline and plan-mode sections are each // appended by a different caller, and each can name a withheld tool. diff --git a/frontend/src/lib/components/copilot/chat/global/workspaceSettingsTools.ts b/frontend/src/lib/components/copilot/chat/global/workspaceSettingsTools.ts new file mode 100644 index 0000000000..d63b75343f --- /dev/null +++ b/frontend/src/lib/components/copilot/chat/global/workspaceSettingsTools.ts @@ -0,0 +1,169 @@ +import { z } from 'zod' +import YAML from 'yaml' +import { ApiError, WorkspaceService, type ApplyWorkspaceSettingsDocumentResponse } from '$lib/gen' +import { createToolDef, type ToolCodeDiff } from '../shared' +import type { SessionTool, SessionToolPolicy } from '../sessionCapabilities' +import { toStableYaml } from './draftDiff' + +// Both endpoints call `require_admin`. +const ADMIN: SessionToolPolicy = ['admin'] + +const MAX_READ_CHARS = 20_000 + +type SettingsChange = ApplyWorkspaceSettingsDocumentResponse['changes'][number] + +const getWorkspaceSettingsSchema = z.object({ + keys: z + .array(z.string()) + .optional() + .describe('Only these top-level keys. Omit for the whole document.') +}) + +const setWorkspaceSettingsSchema = z.object({ + yaml: z + .string() + .describe( + 'A YAML mapping of only the top-level keys to change, each given whole (a nested object replaces the stored one). A key set to null clears it.' + ) +}) + +function apiErrorText(e: unknown): string { + if (e instanceof ApiError) { + return typeof e.body === 'string' ? e.body : JSON.stringify(e.body) + } + return e instanceof Error ? e.message : String(e) +} + +function parseSettingsYaml(text: string): Record { + const doc = YAML.parse(text) + if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) { + throw new Error('The yaml must be a mapping of setting keys to values.') + } + return doc +} + +// An unset value is left out rather than written as null, so a side with nothing set is empty. +function changesSide(changes: SettingsChange[], side: 'before' | 'after'): string { + const set = changes.filter((c) => c[side] != null) + return set.length ? toStableYaml(Object.fromEntries(set.map((c) => [c.key, c[side]]))) : '' +} + +function changesDiff(changes: SettingsChange[]): ToolCodeDiff { + return { + before: changesSide(changes, 'before'), + after: changesSide(changes, 'after'), + lang: 'yaml' + } +} + +// The dry run answers before the confirmation card, and the card shows its changes. Keyed by +// the call's parsed arguments, the one object both hooks are handed. +const dryRuns = new WeakMap() + +export const workspaceSettingsTools: SessionTool<{}>[] = [ + { + requires: ADMIN, + def: createToolDef( + getWorkspaceSettingsSchema, + 'get_workspace_settings', + 'Read the workspace settings as YAML, in the layout of the settings.yaml file the CLI syncs. Secrets read as `__SENSITIVE_AND_UNCHANGED__`.' + ), + planModeSafe: true, + showDetails: true, + fn: async ({ args, workspace, toolId, toolCallbacks }) => { + const parsed = getWorkspaceSettingsSchema.parse(args) + const doc = (await WorkspaceService.getWorkspaceSettingsDocument({ + workspace, + secrets: 'redacted' + })) as Record + let picked = doc + let note = '' + if (parsed.keys?.length) { + picked = Object.fromEntries(parsed.keys.filter((k) => k in doc).map((k) => [k, doc[k]])) + const unset = parsed.keys.filter((k) => !(k in doc)) + if (unset.length) { + note = `\n# Not in the document (unset, or not a setting): ${unset.join(', ')}` + } + } + let text = YAML.stringify(picked, { aliasDuplicateObjects: false, lineWidth: 0 }) + note + if (text.length > MAX_READ_CHARS) { + text = `${text.slice(0, MAX_READ_CHARS)}\n… truncated at ${MAX_READ_CHARS} chars; read the keys you need with \`keys\`.` + } + toolCallbacks.setToolStatus(toolId, { + content: parsed.keys?.length + ? `Read workspace settings: ${parsed.keys.join(', ')}` + : 'Read workspace settings' + }) + return text + } + }, + { + requires: ADMIN, + def: createToolDef( + setWorkspaceSettingsSchema, + 'set_workspace_settings', + 'Change workspace settings. Keys the yaml omits are left as they are. The user reviews the changes before they apply. Read get_instructions subject "workspace_settings" before your first change.' + ), + requiresConfirmation: true, + confirmationMessage: 'Change workspace settings', + cancellationMessage: 'The user declined: no setting was changed.', + showDetails: true, + validateBeforeConfirmation: async ({ args, workspace }) => { + let changes: SettingsChange[] + let warnings: string[] | undefined + try { + const document = parseSettingsYaml(setWorkspaceSettingsSchema.parse(args).yaml) + ;({ changes, warnings } = await WorkspaceService.applyWorkspaceSettingsDocument({ + workspace, + requestBody: { document, replace: false, dry_run: true } + })) + } catch (e) { + const error = apiErrorText(e) + return { label: `Invalid settings: ${error}`, result: `Nothing was changed: ${error}` } + } + if (changes.length === 0) { + return { + label: 'No settings change', + result: `The settings already hold these values: nothing to change.${ + warnings?.length ? ` Warnings: ${warnings.join(' ')}` : '' + }` + } + } + dryRuns.set(args, changes) + return undefined + }, + onConfirmationRequested: ({ args, toolCallbacks, toolId }) => { + const changes = dryRuns.get(args) + if (changes) { + toolCallbacks.setToolStatus(toolId, { codeDiff: changesDiff(changes) }) + } + }, + fn: async ({ args, workspace, toolId, toolCallbacks }) => { + const document = parseSettingsYaml(setWorkspaceSettingsSchema.parse(args).yaml) + toolCallbacks.setToolStatus(toolId, { content: 'Changing workspace settings...' }) + try { + const { changes, warnings } = await WorkspaceService.applyWorkspaceSettingsDocument({ + workspace, + requestBody: { document, replace: false } + }) + const keys = changes.map((c) => c.key) + toolCallbacks.setToolStatus(toolId, { + content: `Changed workspace settings: ${keys.join(', ')}`, + codeDiff: changesDiff(changes) + }) + return JSON.stringify({ + success: true, + message: `The user approved and these settings are now applied: ${keys.join(', ')}.`, + ...(warnings?.length ? { warnings } : {}) + }) + } catch (e) { + const error = apiErrorText(e) + toolCallbacks.setToolStatus(toolId, { + content: 'Failed to change workspace settings', + error + }) + return JSON.stringify({ success: false, error }) + } + } + } +] diff --git a/frontend/src/lib/mcpEndpointTools.ts b/frontend/src/lib/mcpEndpointTools.ts index e0c29f4d7b..2c12e514b9 100644 --- a/frontend/src/lib/mcpEndpointTools.ts +++ b/frontend/src/lib/mcpEndpointTools.ts @@ -65,6 +65,46 @@ export const mcpEndpointTools: EndpointTool[] = [ queryFieldRenames: undefined, bodyFieldRenames: undefined }, + { + name: "getWorkspaceSettingsDocument", + description: "get the workspace settings document: The workspace's settings in the layout of the settings.yaml that `wmill sync` writes. Admin only", + instructions: "", + path: "/w/{workspace}/workspaces/settings_document", + method: "GET", + pathParamsSchema: undefined, + queryParamsSchema: undefined, + bodySchema: undefined, + queryFieldRenames: undefined, + bodyFieldRenames: undefined + }, + { + name: "applyWorkspaceSettingsDocument", + description: "apply a workspace settings document: Applies a settings document, each setting through the endpoint that owns it. Admin only", + instructions: "Send only the top-level keys to change, each whole as getWorkspaceSettingsDocument returns it: a key the document omits is left as it is, and a key set to null clears it. Keep `__SENSITIVE_AND_UNCHANGED__` for a secret you were not given. Call with dry_run true first, show the user the changes and apply only once they agree.", + path: "/w/{workspace}/workspaces/settings_document", + method: "POST", + pathParamsSchema: undefined, + queryParamsSchema: undefined, + bodySchema: { + "type": "object", + "properties": { + "document": { + "type": "object", + "description": "The settings to apply, in the layout of the document. A secret given as `__SENSITIVE_AND_UNCHANGED__` keeps its stored value; one prefixed with `$encrypted:` is decrypted with the workspace key." + }, + "dry_run": { + "description": "Only compute the changes, without applying them.", + "type": "boolean" + } + }, + "required": [ + "document" + ], + "minProperties": 1 +}, + queryFieldRenames: undefined, + bodyFieldRenames: undefined + }, { name: "listDataMetrics", description: "list declared measures and dimensions on DuckLake tables: Call this before writing any aggregate query over a DuckLake table. A declared measure is the canonical definition of that number, and reproducing it yourself will silently disagree with it (a `revenue` measure typically excludes refunds or test rows). Filter by `table` for one table's declarations, or by `path_prefix` (e.g. `f/analytics`) for everything declared under a folder; omit both to browse the whole catalog. Results are keyset-paged: a full page may mean more remain, so continue with the `cursor_*` params rather than assuming a measure does not exist. Use each returned `expr` verbatim, and when a measure has a `filter` write it as `expr FILTER (WHERE filter)` so measures with different predicates can share one GROUP BY. If a number you need has no declared measure, write your own aggregate as usual. Results are limited to declarations whose producing script the caller can read", diff --git a/system_prompts/README.md b/system_prompts/README.md index a651c48692..9051a71112 100644 --- a/system_prompts/README.md +++ b/system_prompts/README.md @@ -47,6 +47,7 @@ a CLI-only workflow file (`flow-cli.md`, `script-cli.md`, `raw-app-cli.md`). | workflow-as-code | `getWorkflowAsCodePrompt(lang)` | `write-workflow-as-code` | `workflow-as-code.md`, the WAC SDK | | pipeline | `getPipelinePrompt()` | `write-pipeline` | `pipeline-base.md` | | data tables | `getDatatablePrompt()` | `datatables` | `datatable.md` | +| workspace settings | `getWorkspaceSettingsPrompt()` | `workspace-settings` | `workspace-settings.md` | | triggers, schedules, preview, CLI commands | — | same names | CLI only | The app chat embeds `RAW_APP_BASE` directly (`chat/app/core.ts`). diff --git a/system_prompts/auto-generated/index.d.ts b/system_prompts/auto-generated/index.d.ts index 8ba83f7f74..fee83d38b5 100644 --- a/system_prompts/auto-generated/index.d.ts +++ b/system_prompts/auto-generated/index.d.ts @@ -5,5 +5,6 @@ export declare function getResourcePrompt(): string; export declare function getRawAppPrompt(language?: string): string; export declare function getPipelinePrompt(): string; export declare function getDatatablePrompt(): string; +export declare function getWorkspaceSettingsPrompt(): string; export declare function getDatatableSdkReference(language?: string): string; export declare function getWorkflowAsCodePrompt(language?: string): string; diff --git a/system_prompts/auto-generated/index.ts b/system_prompts/auto-generated/index.ts index 60bb79b031..da3b622046 100644 --- a/system_prompts/auto-generated/index.ts +++ b/system_prompts/auto-generated/index.ts @@ -80,6 +80,13 @@ export function getDatatablePrompt(): string { ].filter(Boolean).join('\n\n'); } +// Helper for the workspace settings document (chat consumers) +export function getWorkspaceSettingsPrompt(): string { + return [ + prompts.WORKSPACE_SETTINGS_BASE + ].filter(Boolean).join('\n\n'); +} + // Helper to get the datatable SQL SDK reference (wmill.datatable()). // Pass a language to get only that SDK; omit it to get both. export function getDatatableSdkReference(language?: string): string { diff --git a/system_prompts/auto-generated/prompts.d.ts b/system_prompts/auto-generated/prompts.d.ts index cfdde52495..5cb7e336b3 100644 --- a/system_prompts/auto-generated/prompts.d.ts +++ b/system_prompts/auto-generated/prompts.d.ts @@ -6,6 +6,7 @@ export declare const RESOURCES_BASE: string; export declare const RAW_APP_BASE: string; export declare const PIPELINE_BASE: string; export declare const DATATABLE_BASE: string; +export declare const WORKSPACE_SETTINGS_BASE: string; export declare const WORKFLOW_AS_CODE_BASE: string; export declare const FLOW_CHAT_SPECIAL_MODULES: string; export declare const SDK_TYPESCRIPT: string; diff --git a/system_prompts/auto-generated/prompts.ts b/system_prompts/auto-generated/prompts.ts index 83355cd6b0..7ea9e6bfeb 100644 --- a/system_prompts/auto-generated/prompts.ts +++ b/system_prompts/auto-generated/prompts.ts @@ -1411,6 +1411,66 @@ A data table under roles is reached as a Postgres role, and Postgres enforces wh Read the Permissions section of https://www.windmill.dev/docs/core_concepts/persistent_storage/data_tables before designing who reaches what: it covers tenants, grants, the default role and the roles a connection that names none ends up holding, rows that differ per viewer through row-level security, and how forks and raw apps behave under roles. `; +export const WORKSPACE_SETTINGS_BASE = `# Workspace settings + +Everything an admin sets on the Workspace settings page is one document: a mapping of top-level +keys, the same layout as the \`settings.yaml\` file \`wmill sync\` keeps. Only a workspace admin can +read or change it. + +## Applying a document + +- A partial document changes only the keys it states; every key it omits is left as it is. +- Each key is replaced whole, so read its current value and send it back edited: a nested object + given with one field replaces the stored object with that one field. +- A key set to \`null\` clears that setting; an on/off switch cleared this way reads as \`false\`. +- Each setting is written through the same path the settings page uses, with the same validation + and audit log. The writes are not one transaction: when one fails the apply stops, and the + error says what was already written. +- A dry run reports the changes, as \`before\` / \`after\` per key, without writing anything. +- \`name\` is never applied: rename a workspace on the settings page. \`color\` is applied only when + set to a value, so \`null\` does not clear it. +- A key the server does not know is ignored and named in the warnings: check them, since a typo + otherwise reads as "nothing to change". +- A setting this edition does not offer (Enterprise only) is refused, by the dry run too. + +## Secrets + +\`slack_oauth_client_secret\` is the one secret. A read shows \`__SENSITIVE_AND_UNCHANGED__\` in place +of a set secret; sending that placeholder back keeps the stored value. A value starting with +\`$encrypted:\` is decrypted with the workspace key, and one encrypted with another workspace's key +is left unchanged with a warning; any other value is taken as plaintext. Never +write a secret you were not given: keep the placeholder. + +## Keys + +| Key | Value | +|---|---| +| \`auto_invite\` | \`{ enabled, mode: invite \\| add, operator, instance_groups?, instance_groups_roles? }\`. The instance groups are managed only when \`instance_groups\` is stated; clearing a group removes the members it added. | +| \`error_handler\` / \`success_handler\` | \`{ path, extra_args?, muted_on_cancel?, muted_on_user_path? }\`, \`path\` being \`script/\` or \`flow/\`. | +| \`error_handler_fallback_to_instance_alerts\` | Report failures to the instance critical alert channels when no error handler is set. | +| \`webhook\` | URL POSTed a JSON event on every change to the workspace's items. | +| \`ai_config\` | Providers, default and completion models, custom prompts, \`copilot_disabled\`. | +| \`large_file_storage\` | Workspace object storage: \`{ type, s3_resource_path \\| azure_blob_resource_path \\| gcs_resource_path, public_resource?, secondary_storage?, advanced_permissions? }\`. | +| \`git_sync\` | \`{ repositories: [...] }\`, each with \`git_repo_resource_path\`, \`script_path\` and \`settings\` (include/exclude paths and types). Server-owned fields (webhook secrets, sync state, credentials) are never read nor written. | +| \`datatable\` | \`{ datatables: { : { database: { resource_type, resource_path? } } } }\`. Role permissions are \`datatable_permissions\`. | +| \`datatable_permissions\` | \`{ : { default_role, roles: { : [tenants] } } }\`: who may connect to each data table as which role. Tenants are \`u/\`, \`g/\`, \`f/\` or \`*\`; \`admin\` is always a role, and \`default_role: null\` means no default. A data table the value omits is no longer under roles. Only a data table on a cluster Windmill manages can be. | +| \`protection_rules\` | \`{ : { rules: [...], bypass_groups: [...], bypass_users: [...] } }\`, rules among \`DisableDirectDeployment\`, \`DisableWorkspaceForking\`, \`RestrictDeployToDeployers\`, \`RestrictAnonymousAppDeployment\`, \`RestrictPublicRunSharing\`, \`RestrictGuestAppDeployment\`. A rule the value omits is deleted. The dev workspace lock is not listed and cannot be set here. | +| \`ducklake\` | \`{ ducklakes: { : { catalog, storage, ... } } }\`. | +| \`dbt_warehouses\` | dbt warehouse connections. | +| \`default_app\` | Path of the app shown as the workspace home. | +| \`default_scripts\` | \`{ order?, hidden?, default_script_content? }\` for the script language picker. | +| \`deploy_ui\` | \`{ include_path?, include_type? }\`: what the deploy-to-workspace UI offers. | +| \`operator_settings\` | What operators see and do: page visibility flags (\`runs\`, \`schedules\`, \`resources\`, \`variables\`, \`assets\`, \`audit_logs\`, \`groups\`, \`folders\`, \`workers\`, \`triggers\`) and write rights (\`manage_schedules\`, \`manage_triggers\`, \`builder_flows\`, \`builder_apps\`). A write right the value omits keeps its stored value. | +| \`mute_critical_alerts\` | Mute the workspace's critical alerts. | +| \`slack_team_id\`, \`slack_name\` | Read only: set by connecting Slack on the settings page. | +| \`slack_command_script\` / \`teams_command_script\` | Script or flow handling the Slack / Teams command. | +| \`slack_oauth_client_id\` / \`slack_oauth_client_secret\` | The workspace's own Slack OAuth app. Clearing either removes both. | +| \`public_app_execution_limit_per_minute\` | Rate limit for public apps, \`null\` for none. | +| \`guest_access_enabled\`, \`guest_jwt_public_key\`, \`guest_jwt_jwks_url\` | Guest access to public apps and the key that signs guest JWTs. | +| \`add_admins_and_developers_to_forks\` | Add the workspace's admins and developers to each fork. | +| \`color\` | Workspace color, \`#rrggbb\`. | +`; + export const WORKFLOW_AS_CODE_BASE = `# Windmill Workflow-as-Code Writing Guide ## Scope diff --git a/system_prompts/auto-generated/skills/workspace-settings/SKILL.md b/system_prompts/auto-generated/skills/workspace-settings/SKILL.md new file mode 100644 index 0000000000..8c0e5b4b70 --- /dev/null +++ b/system_prompts/auto-generated/skills/workspace-settings/SKILL.md @@ -0,0 +1,84 @@ +--- +name: workspace-settings +description: MUST use when reading or changing workspace settings, or the settings.yaml file `wmill sync` keeps. +--- + +# Workspace settings + +Everything an admin sets on the Workspace settings page is one document: a mapping of top-level +keys, the same layout as the `settings.yaml` file `wmill sync` keeps. Only a workspace admin can +read or change it. + +## Applying a document + +- A partial document changes only the keys it states; every key it omits is left as it is. +- Each key is replaced whole, so read its current value and send it back edited: a nested object + given with one field replaces the stored object with that one field. +- A key set to `null` clears that setting; an on/off switch cleared this way reads as `false`. +- Each setting is written through the same path the settings page uses, with the same validation + and audit log. The writes are not one transaction: when one fails the apply stops, and the + error says what was already written. +- A dry run reports the changes, as `before` / `after` per key, without writing anything. +- `name` is never applied: rename a workspace on the settings page. `color` is applied only when + set to a value, so `null` does not clear it. +- A key the server does not know is ignored and named in the warnings: check them, since a typo + otherwise reads as "nothing to change". +- A setting this edition does not offer (Enterprise only) is refused, by the dry run too. + +## Secrets + +`slack_oauth_client_secret` is the one secret. A read shows `__SENSITIVE_AND_UNCHANGED__` in place +of a set secret; sending that placeholder back keeps the stored value. A value starting with +`$encrypted:` is decrypted with the workspace key, and one encrypted with another workspace's key +is left unchanged with a warning; any other value is taken as plaintext. Never +write a secret you were not given: keep the placeholder. + +## Keys + +| Key | Value | +|---|---| +| `auto_invite` | `{ enabled, mode: invite \| add, operator, instance_groups?, instance_groups_roles? }`. The instance groups are managed only when `instance_groups` is stated; clearing a group removes the members it added. | +| `error_handler` / `success_handler` | `{ path, extra_args?, muted_on_cancel?, muted_on_user_path? }`, `path` being `script/` or `flow/`. | +| `error_handler_fallback_to_instance_alerts` | Report failures to the instance critical alert channels when no error handler is set. | +| `webhook` | URL POSTed a JSON event on every change to the workspace's items. | +| `ai_config` | Providers, default and completion models, custom prompts, `copilot_disabled`. | +| `large_file_storage` | Workspace object storage: `{ type, s3_resource_path \| azure_blob_resource_path \| gcs_resource_path, public_resource?, secondary_storage?, advanced_permissions? }`. | +| `git_sync` | `{ repositories: [...] }`, each with `git_repo_resource_path`, `script_path` and `settings` (include/exclude paths and types). Server-owned fields (webhook secrets, sync state, credentials) are never read nor written. | +| `datatable` | `{ datatables: { : { database: { resource_type, resource_path? } } } }`. Role permissions are `datatable_permissions`. | +| `datatable_permissions` | `{ : { default_role, roles: { : [tenants] } } }`: who may connect to each data table as which role. Tenants are `u/`, `g/`, `f/` or `*`; `admin` is always a role, and `default_role: null` means no default. A data table the value omits is no longer under roles. Only a data table on a cluster Windmill manages can be. | +| `protection_rules` | `{ : { rules: [...], bypass_groups: [...], bypass_users: [...] } }`, rules among `DisableDirectDeployment`, `DisableWorkspaceForking`, `RestrictDeployToDeployers`, `RestrictAnonymousAppDeployment`, `RestrictPublicRunSharing`, `RestrictGuestAppDeployment`. A rule the value omits is deleted. The dev workspace lock is not listed and cannot be set here. | +| `ducklake` | `{ ducklakes: { : { catalog, storage, ... } } }`. | +| `dbt_warehouses` | dbt warehouse connections. | +| `default_app` | Path of the app shown as the workspace home. | +| `default_scripts` | `{ order?, hidden?, default_script_content? }` for the script language picker. | +| `deploy_ui` | `{ include_path?, include_type? }`: what the deploy-to-workspace UI offers. | +| `operator_settings` | What operators see and do: page visibility flags (`runs`, `schedules`, `resources`, `variables`, `assets`, `audit_logs`, `groups`, `folders`, `workers`, `triggers`) and write rights (`manage_schedules`, `manage_triggers`, `builder_flows`, `builder_apps`). A write right the value omits keeps its stored value. | +| `mute_critical_alerts` | Mute the workspace's critical alerts. | +| `slack_team_id`, `slack_name` | Read only: set by connecting Slack on the settings page. | +| `slack_command_script` / `teams_command_script` | Script or flow handling the Slack / Teams command. | +| `slack_oauth_client_id` / `slack_oauth_client_secret` | The workspace's own Slack OAuth app. Clearing either removes both. | +| `public_app_execution_limit_per_minute` | Rate limit for public apps, `null` for none. | +| `guest_access_enabled`, `guest_jwt_public_key`, `guest_jwt_jwks_url` | Guest access to public apps and the key that signs guest JWTs. | +| `add_admins_and_developers_to_forks` | Add the workspace's admins and developers to each fork. | +| `color` | Workspace color, `#rrggbb`. | + +## From the command line + +`wmill workspace get-settings` prints the document (`--encrypted` for secrets encrypted with the +workspace key, `--json` for JSON). `wmill workspace apply-settings ` applies a partial +document from a YAML or JSON file: it shows the changes and asks before writing (`--dry-run` to +only show them, `-y` to skip the question, `--replace` to clear what the file omits). + +## In a synced repository + +With `includeSettings: true` in `wmill.yaml`, `wmill sync pull` writes the document to +`settings.yaml`, secrets encrypted with the workspace key (plaintext only with `plainSecrets`), and +`wmill sync push` applies it. Unlike a partial apply, a push treats the file as the whole +document: a setting the file omits is cleared, except `operator_settings`, `color`, the auto-invite +instance groups, and the keys `ducklake`, `dbt_warehouses`, `deploy_ui`, `teams_command_script`, +`public_app_execution_limit_per_minute`, `guest_access_enabled`, `guest_jwt_public_key`, +`guest_jwt_jwks_url`, `error_handler_fallback_to_instance_alerts` and +`add_admins_and_developers_to_forks`, which are managed only when the file states them. +`datatable_permissions` is pushed only with `includeDatatablePermissions: true` (or +`--include-datatable-permissions`). A push ignores `protection_rules`: `protection-rules.yaml` and +`wmill protection-rules push` sync them. diff --git a/system_prompts/base/workspace-settings.md b/system_prompts/base/workspace-settings.md new file mode 100644 index 0000000000..ad5838dad2 --- /dev/null +++ b/system_prompts/base/workspace-settings.md @@ -0,0 +1,81 @@ +# Workspace settings + +Everything an admin sets on the Workspace settings page is one document: a mapping of top-level +keys, the same layout as the `settings.yaml` file `wmill sync` keeps. Only a workspace admin can +read or change it. + +## Applying a document + +- A partial document changes only the keys it states; every key it omits is left as it is. +- Each key is replaced whole, so read its current value and send it back edited: a nested object + given with one field replaces the stored object with that one field. +- A key set to `null` clears that setting; an on/off switch cleared this way reads as `false`. +- Each setting is written through the same path the settings page uses, with the same validation + and audit log. The writes are not one transaction: when one fails the apply stops, and the + error says what was already written. +- A dry run reports the changes, as `before` / `after` per key, without writing anything. +- `name` is never applied: rename a workspace on the settings page. `color` is applied only when + set to a value, so `null` does not clear it. +- A key the server does not know is ignored and named in the warnings: check them, since a typo + otherwise reads as "nothing to change". +- A setting this edition does not offer (Enterprise only) is refused, by the dry run too. + +## Secrets + +`slack_oauth_client_secret` is the one secret. A read shows `__SENSITIVE_AND_UNCHANGED__` in place +of a set secret; sending that placeholder back keeps the stored value. A value starting with +`$encrypted:` is decrypted with the workspace key, and one encrypted with another workspace's key +is left unchanged with a warning; any other value is taken as plaintext. Never +write a secret you were not given: keep the placeholder. + +## Keys + +| Key | Value | +|---|---| +| `auto_invite` | `{ enabled, mode: invite \| add, operator, instance_groups?, instance_groups_roles? }`. The instance groups are managed only when `instance_groups` is stated; clearing a group removes the members it added. | +| `error_handler` / `success_handler` | `{ path, extra_args?, muted_on_cancel?, muted_on_user_path? }`, `path` being `script/` or `flow/`. | +| `error_handler_fallback_to_instance_alerts` | Report failures to the instance critical alert channels when no error handler is set. | +| `webhook` | URL POSTed a JSON event on every change to the workspace's items. | +| `ai_config` | Providers, default and completion models, custom prompts, `copilot_disabled`. | +| `large_file_storage` | Workspace object storage: `{ type, s3_resource_path \| azure_blob_resource_path \| gcs_resource_path, public_resource?, secondary_storage?, advanced_permissions? }`. | +| `git_sync` | `{ repositories: [...] }`, each with `git_repo_resource_path`, `script_path` and `settings` (include/exclude paths and types). Server-owned fields (webhook secrets, sync state, credentials) are never read nor written. | +| `datatable` | `{ datatables: { : { database: { resource_type, resource_path? } } } }`. Role permissions are `datatable_permissions`. | +| `datatable_permissions` | `{ : { default_role, roles: { : [tenants] } } }`: who may connect to each data table as which role. Tenants are `u/`, `g/`, `f/` or `*`; `admin` is always a role, and `default_role: null` means no default. A data table the value omits is no longer under roles. Only a data table on a cluster Windmill manages can be. | +| `protection_rules` | `{ : { rules: [...], bypass_groups: [...], bypass_users: [...] } }`, rules among `DisableDirectDeployment`, `DisableWorkspaceForking`, `RestrictDeployToDeployers`, `RestrictAnonymousAppDeployment`, `RestrictPublicRunSharing`, `RestrictGuestAppDeployment`. A rule the value omits is deleted. The dev workspace lock is not listed and cannot be set here. | +| `ducklake` | `{ ducklakes: { : { catalog, storage, ... } } }`. | +| `dbt_warehouses` | dbt warehouse connections. | +| `default_app` | Path of the app shown as the workspace home. | +| `default_scripts` | `{ order?, hidden?, default_script_content? }` for the script language picker. | +| `deploy_ui` | `{ include_path?, include_type? }`: what the deploy-to-workspace UI offers. | +| `operator_settings` | What operators see and do: page visibility flags (`runs`, `schedules`, `resources`, `variables`, `assets`, `audit_logs`, `groups`, `folders`, `workers`, `triggers`) and write rights (`manage_schedules`, `manage_triggers`, `builder_flows`, `builder_apps`). A write right the value omits keeps its stored value. | +| `mute_critical_alerts` | Mute the workspace's critical alerts. | +| `slack_team_id`, `slack_name` | Read only: set by connecting Slack on the settings page. | +| `slack_command_script` / `teams_command_script` | Script or flow handling the Slack / Teams command. | +| `slack_oauth_client_id` / `slack_oauth_client_secret` | The workspace's own Slack OAuth app. Clearing either removes both. | +| `public_app_execution_limit_per_minute` | Rate limit for public apps, `null` for none. | +| `guest_access_enabled`, `guest_jwt_public_key`, `guest_jwt_jwks_url` | Guest access to public apps and the key that signs guest JWTs. | +| `add_admins_and_developers_to_forks` | Add the workspace's admins and developers to each fork. | +| `color` | Workspace color, `#rrggbb`. | + + +## From the command line + +`wmill workspace get-settings` prints the document (`--encrypted` for secrets encrypted with the +workspace key, `--json` for JSON). `wmill workspace apply-settings ` applies a partial +document from a YAML or JSON file: it shows the changes and asks before writing (`--dry-run` to +only show them, `-y` to skip the question, `--replace` to clear what the file omits). + +## In a synced repository + +With `includeSettings: true` in `wmill.yaml`, `wmill sync pull` writes the document to +`settings.yaml`, secrets encrypted with the workspace key (plaintext only with `plainSecrets`), and +`wmill sync push` applies it. Unlike a partial apply, a push treats the file as the whole +document: a setting the file omits is cleared, except `operator_settings`, `color`, the auto-invite +instance groups, and the keys `ducklake`, `dbt_warehouses`, `deploy_ui`, `teams_command_script`, +`public_app_execution_limit_per_minute`, `guest_access_enabled`, `guest_jwt_public_key`, +`guest_jwt_jwks_url`, `error_handler_fallback_to_instance_alerts` and +`add_admins_and_developers_to_forks`, which are managed only when the file states them. +`datatable_permissions` is pushed only with `includeDatatablePermissions: true` (or +`--include-datatable-permissions`). A push ignores `protection_rules`: `protection-rules.yaml` and +`wmill protection-rules push` sync them. + diff --git a/system_prompts/generate.py b/system_prompts/generate.py index 055f03d543..7249954d01 100644 --- a/system_prompts/generate.py +++ b/system_prompts/generate.py @@ -1702,6 +1702,12 @@ TOPICS: dict[str, dict] = { 'chat_helper': 'getDatatablePrompt', 'parts': ['datatable.md'], }, + 'workspace_settings': { + 'skill': 'workspace-settings', + 'description': 'MUST use when reading or changing workspace settings, or the settings.yaml file `wmill sync` keeps.', + 'chat_helper': 'getWorkspaceSettingsPrompt', + 'parts': ['workspace-settings.md'], + }, 'cli_commands': { 'skill': 'cli-commands', 'description': 'MUST use when using the CLI, including debugging job failures and inspecting run history via `wmill job`.', @@ -1722,6 +1728,7 @@ CHAT_CONSTANTS = { 'raw-app.md': 'RAW_APP_BASE', 'pipeline-base.md': 'PIPELINE_BASE', 'datatable.md': 'DATATABLE_BASE', + 'workspace-settings.md': 'WORKSPACE_SETTINGS_BASE', 'workflow-as-code.md': 'WORKFLOW_AS_CODE_BASE', 'flow-chat-special-modules.md': 'FLOW_CHAT_SPECIAL_MODULES', } @@ -2662,6 +2669,11 @@ export function getDatatablePrompt(): string { __RETURN_datatable__ } +// Helper for the workspace settings document (chat consumers) +export function getWorkspaceSettingsPrompt(): string { + __RETURN_workspace_settings__ +} + // Helper to get the datatable SQL SDK reference (wmill.datatable()). // Pass a language to get only that SDK; omit it to get both. export function getDatatableSdkReference(language?: string): string { @@ -2724,6 +2736,7 @@ export declare function getResourcePrompt(): string; export declare function getRawAppPrompt(language?: string): string; export declare function getPipelinePrompt(): string; export declare function getDatatablePrompt(): string; +export declare function getWorkspaceSettingsPrompt(): string; export declare function getDatatableSdkReference(language?: string): string; export declare function getWorkflowAsCodePrompt(language?: string): string; """