Files
windmill/frontend/src/lib/roleCapabilities.ts
T
d6c2b2c683 feat: add a use/plan/build mode picker and read-only prod sessions (#11940)
* feat: read-only ai sessions on protected workspaces with a use, plan and build picker

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: address local review findings on the use, plan and build picker

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: address CI review findings on use mode and protected sessions

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: hold test-run forms and pending protection lookups to use mode

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor: fetch rules once per send and dedupe helpers and prompt text

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: send role-locked operators to an admin and cover it with an eval

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: count the work mode each session message runs in

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: regenerate the mcp session definitions after merging main

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: rename build to use & build and let the chat ask to switch modes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: word a declined mode switch by the switch, not the mode

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to 55445977dd05c25791388070182c77f4dbc20c0e

This commit updates the EE repository reference after PR #915 was merged in windmill-ee-private.

Previous ee-repo-ref: 7f7bf7003e356346ce74395af02c5bdf14f38781

New ee-repo-ref: 55445977dd05c25791388070182c77f4dbc20c0e

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-10-23 15:53:04 +02:00

88 lines
3.3 KiB
TypeScript

import type { OperatorSettings, RoleCapabilities, RoleVisibility } from '$lib/gen'
import type { UserExt, UserWorkspace } from '$lib/stores'
import { FULL_CAPABILITIES, VISIBILITY_KEYS } from '$lib/workspaceRoles'
/**
* What a member's workspace role lets them author, manage and see: the frontend's copy of
* `windmill_common::roles`, see `docs/workspace-roles.md`. Gate on these, never on
* `UserExt.operator`, which only says "this role cannot write code" and so lets through a role
* that may code but not, say, build apps.
*/
export type VisibilityKey = keyof RoleVisibility
export { FULL_CAPABILITIES }
const VISIBILITY = VISIBILITY_KEYS.map((v) => v.key)
/**
* The operator role as a server that predates roles describes it, through `operator_settings`:
* builder rights are `compose`, the manage rights are granted unless withdrawn, and a page is
* visible only when its flag is `true` (absent settings hide every page, as they always did).
*/
export function legacyOperatorCapabilities(
settings: OperatorSettings | null | undefined
): RoleCapabilities {
const visibility = Object.fromEntries(
VISIBILITY.map((k) => [k, settings?.[k] === true])
) as RoleVisibility
return {
code: false,
flows: settings?.builder_flows === true ? 'compose' : 'none',
apps: settings?.builder_apps === true ? 'compose' : 'none',
manage_schedules: settings?.manage_schedules !== false,
manage_triggers: settings?.manage_triggers !== false,
visibility
}
}
export type CapabilitiesUser = Partial<Pick<UserExt, 'is_admin' | 'is_super_admin' | 'operator'>>
export type CapabilitiesWorkspace = Pick<UserWorkspace, 'capabilities' | 'operator_settings'>
/**
* The capabilities of `user` in `workspace` (its entry of `$userWorkspaces`). An admin holds
* everything. The server's `capabilities` win; an older server sends none, and the role is then
* derived from `operator` and `operator_settings` the way it read them. Without a user, a
* non-null `operator_settings` is what marks an operator, as the server sends it to them only.
*/
export function capabilitiesIn(
user: CapabilitiesUser | undefined,
workspace: CapabilitiesWorkspace | undefined
): RoleCapabilities {
if (user?.is_admin || user?.is_super_admin) return FULL_CAPABILITIES
if (workspace?.capabilities) return workspace.capabilities
const operator = user?.operator ?? workspace?.operator_settings != null
return operator ? legacyOperatorCapabilities(workspace?.operator_settings) : FULL_CAPABILITIES
}
/** Whether the role is gated at all, as `RoleCapabilities::is_restricted`: visibility is left out,
* since hiding a page must not cost a full author what restricted roles are refused, such as
* forking. */
export function isRestricted(caps: RoleCapabilities): boolean {
return (
!caps.code ||
caps.flows !== 'full' ||
caps.apps !== 'full' ||
!caps.manage_schedules ||
!caps.manage_triggers
)
}
/** The role with nothing left to build and the same pages in view: what a session in Use mode
* acts as. */
export function withoutBuilding(caps: RoleCapabilities): RoleCapabilities {
return {
...caps,
code: false,
flows: 'none',
apps: 'none',
manage_schedules: false,
manage_triggers: false
}
}
/** Whether the role hides any page. */
export function hidesPages(caps: RoleCapabilities): boolean {
return VISIBILITY.some((k) => !caps.visibility[k])
}