mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-15 16:03:02 +00:00
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 <noreply@anthropic.com> * docs: cover wmill workspace settings commands in the settings guidance Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: say unknown settings keys are warned and ee-only ones refused Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: cover datatable_permissions and protection_rules in the settings guidance Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a2ee052e03
commit
468cf1d994
@@ -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:
|
||||
|
||||
@@ -56,6 +56,46 @@ pub fn all_tools() -> Vec<EndpointTool> {
|
||||
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"),
|
||||
|
||||
Generated
+86
@@ -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 <name>\`, 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/<path>\` or \`flow/<path>\`. |
|
||||
| \`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: { <name>: { database: { resource_type, resource_path? } } } }\`. Role permissions are \`datatable_permissions\`. |
|
||||
| \`datatable_permissions\` | \`{ <data table>: { default_role, roles: { <role>: [tenants] } } }\`: who may connect to each data table as which role. Tenants are \`u/<user>\`, \`g/<group>\`, \`f/<folder>\` 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\` | \`{ <rule name>: { 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: { <name>: { 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 <file>\` 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
|
||||
|
||||
@@ -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}
|
||||
<ToolCodeDiffView {diff} diffLines={lines} streaming={isStreaming} />
|
||||
{/if}
|
||||
{#if awaitingConfirmation}
|
||||
<ToolConfirmationFooter
|
||||
class="px-3 py-2 border-t border-border-light"
|
||||
toolCallId={message.tool_call_id}
|
||||
rejectIcon={XCircle}
|
||||
rejectDestructive
|
||||
confirmLabel="Apply"
|
||||
confirmIcon={Check}
|
||||
/>
|
||||
{/if}
|
||||
{#if message.error}
|
||||
<div
|
||||
class="px-3 py-2 text-2xs text-red-600 dark:text-red-400 whitespace-pre-wrap break-words {hasDiff
|
||||
|
||||
@@ -128,6 +128,7 @@ import {
|
||||
import { MAX_SKILL_DESCRIPTION_LENGTH, MAX_SKILL_INSTRUCTIONS_LENGTH } from '../skills/skillMd'
|
||||
import {
|
||||
getDatatablePrompt,
|
||||
getWorkspaceSettingsPrompt,
|
||||
getDatatableSdkReference,
|
||||
getFlowPrompt,
|
||||
getPipelinePrompt,
|
||||
@@ -218,6 +219,7 @@ import {
|
||||
getDraftDiffValues
|
||||
} from '$lib/utils_draft_deploy'
|
||||
import { changedLineIndices, draftDeployedPatch, windowPatch } from './draftDiff'
|
||||
import { workspaceSettingsTools } from './workspaceSettingsTools'
|
||||
import { getRun, summarizeRun } from './flowRunTree'
|
||||
import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte'
|
||||
import { invalidateWorkspaceComparison } from '$lib/workspaceComparison'
|
||||
@@ -318,7 +320,8 @@ const INSTRUCTION_SUBJECTS = [
|
||||
// datatable SDK reference (the wmill.datatable() runnable API) the same way.
|
||||
// `pipeline` likewise isn't an item type — a data pipeline is a set of
|
||||
// annotated scripts in a folder, so it gets authoring guidance, not a CRUD type.
|
||||
const INSTRUCTION_SUBJECTS_EXTRA = ['datatable', 'pipeline'] as const
|
||||
// `workspace_settings` is the settings document an admin reads and changes.
|
||||
const INSTRUCTION_SUBJECTS_EXTRA = ['datatable', 'pipeline', 'workspace_settings'] as const
|
||||
const ALL_INSTRUCTION_SUBJECTS = [...INSTRUCTION_SUBJECTS, ...INSTRUCTION_SUBJECTS_EXTRA] as const
|
||||
const MAX_LIST_LIMIT = 100
|
||||
type ActiveGlobalEditorType = Extract<WorkspaceItemType, 'script' | 'flow' | 'app'>
|
||||
@@ -378,7 +381,7 @@ const scriptLangSchema = z.enum($ScriptLang.enum)
|
||||
|
||||
const getInstructionsSchema = z.object({
|
||||
subject: instructionSubjectSchema.describe(
|
||||
'What to get authoring instructions for: a workspace item type (script, flow, resource, app), "pipeline" for building a data pipeline (a DAG of annotated scripts wired by storage assets — NOT a flow), or "datatable" for how data tables work (schema changes and migrations, roles and permissions) plus the wmill.datatable() SQL SDK used inside runnables. Schedules, triggers, and variables don\'t need instructions — their tool schemas describe everything.'
|
||||
'What to get authoring instructions for: a workspace item type (script, flow, resource, app), "pipeline" for building a data pipeline (a DAG of annotated scripts wired by storage assets — NOT a flow), or "datatable" for how data tables work (schema changes and migrations, roles and permissions) plus the wmill.datatable() SQL SDK used inside runnables, or "workspace_settings" for the keys and rules of the workspace settings (admins only). Schedules, triggers, and variables don\'t need instructions — their tool schemas describe everything.'
|
||||
),
|
||||
language: scriptLangSchema
|
||||
.optional()
|
||||
@@ -2670,6 +2673,8 @@ function getInstructions(
|
||||
return getDatatableInstructions(language)
|
||||
case 'pipeline':
|
||||
return getPipelineInstructions()
|
||||
case 'workspace_settings':
|
||||
return getWorkspaceSettingsPrompt()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3614,6 +3619,10 @@ export const globalTools: SessionTool<{}>[] = [
|
||||
// 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<string, unknown> {
|
||||
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<object, SettingsChange[]>()
|
||||
|
||||
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<string, unknown>
|
||||
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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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",
|
||||
|
||||
@@ -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`).
|
||||
|
||||
+1
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+1
@@ -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;
|
||||
|
||||
@@ -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/<path>\` or \`flow/<path>\`. |
|
||||
| \`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: { <name>: { database: { resource_type, resource_path? } } } }\`. Role permissions are \`datatable_permissions\`. |
|
||||
| \`datatable_permissions\` | \`{ <data table>: { default_role, roles: { <role>: [tenants] } } }\`: who may connect to each data table as which role. Tenants are \`u/<user>\`, \`g/<group>\`, \`f/<folder>\` 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\` | \`{ <rule name>: { 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: { <name>: { 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
|
||||
|
||||
@@ -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/<path>` or `flow/<path>`. |
|
||||
| `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: { <name>: { database: { resource_type, resource_path? } } } }`. Role permissions are `datatable_permissions`. |
|
||||
| `datatable_permissions` | `{ <data table>: { default_role, roles: { <role>: [tenants] } } }`: who may connect to each data table as which role. Tenants are `u/<user>`, `g/<group>`, `f/<folder>` 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` | `{ <rule name>: { 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: { <name>: { 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 <file>` 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.
|
||||
@@ -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/<path>` or `flow/<path>`. |
|
||||
| `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: { <name>: { database: { resource_type, resource_path? } } } }`. Role permissions are `datatable_permissions`. |
|
||||
| `datatable_permissions` | `{ <data table>: { default_role, roles: { <role>: [tenants] } } }`: who may connect to each data table as which role. Tenants are `u/<user>`, `g/<group>`, `f/<folder>` 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` | `{ <rule name>: { 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: { <name>: { 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`. |
|
||||
<!-- cli-only -->
|
||||
|
||||
## 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 <file>` 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-only -->
|
||||
@@ -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;
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user