mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
feat: let operators compose flows when the workspace grants the right
Adds operator_settings.builder_flows: a workspace setting that lets every operator compose flows out of runnables that already exist. It does not make them authors. The boundary the operator role draws is authoring code and running arbitrary code, and this does not move it: check_flow_is_composition_only walks the value and refuses anything carrying code, including the shapes an obvious walk misses (code hoisted into a flow_node, an AI agent step's tools, and a linked ai_agent resource whose tool list is resolved at run time). What the walk cannot settle it returns for the caller to authorize under RLS: the worker tags the steps pin, every runnable they reference, and the (path, hash) of every version-pinned step. Composing a path is enough to run it and to run it as whoever it runs as, since the worker resolves a step's path with the root DB handle and adopts that runnable's on_behalf_of. A pinned hash needs its own check because dispatch ignores the path beside it. The gate runs on every write and on both request-supplied-value paths, flow preview and flow dependencies, or either becomes the way to run what the write path refuses. Operators of a builder workspace consume a full author seat; the EE companion carries the counting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dsf6VC4MVLisiEoeQkgbr4
This commit is contained in:
co-authored by
Claude Opus 5
parent
625116e378
commit
0df128cc93
@@ -0,0 +1,60 @@
|
||||
# Operator builder rights
|
||||
|
||||
`operator_settings.builder_flows` lets every operator of a workspace compose flows out of runnables
|
||||
that already exist. It does not make them authors: the boundary the operator role draws is
|
||||
**authoring code and running arbitrary code**, and this does not move it.
|
||||
|
||||
It is a write right, unlike the visibility flags beside it, and unlike the withdrawable rights in
|
||||
`docs/operator-write-rights.md` it is granted on request and costs a seat. Read it with
|
||||
`windmill_common::workspaces::operator_can_build_flows` (60s cache, shared with the withdrawable
|
||||
rights) and gate a write with `check_operator_can_build_flows`.
|
||||
|
||||
## What the check has to cover
|
||||
|
||||
`check_flow_is_composition_only` (`windmill-common/src/flows.rs`) walks a `FlowValue` and refuses
|
||||
anything that carries code. Three of its rules exist because the obvious walk misses them:
|
||||
|
||||
- **`FlowScript` and any populated `modules_node` / `default_node`.** These name code hoisted into
|
||||
a `flow_node` row. Only the dependency job produces them, so an authored value carrying one
|
||||
names code stored under some other flow. The walk covers `modules`, so a node reference is a way
|
||||
past it.
|
||||
- **An AI agent step's `tools`.** `ToolValue::FlowModule` wraps a whole `FlowModuleValue`, so a
|
||||
tool can be a raw script.
|
||||
- **An AI agent step's `agent` link.** A linked agent resolves its tools from an `ai_agent`
|
||||
resource at run time, and operators may write resources, so the tool list is outside this check
|
||||
and can be swapped for a raw script after the flow is approved.
|
||||
|
||||
It also returns what a value-only walk cannot authorize, for the caller to check against its own
|
||||
permissions:
|
||||
|
||||
- **the worker tags the steps pin**, or a builder routes a job onto a privileged worker group;
|
||||
- **every runnable a step references**. `script_to_payload` resolves a step's path with the root DB
|
||||
handle (`db_authed = None`) and returns the referenced runnable's `on_behalf_of`, which
|
||||
`worker_flow` then applies to the step job. So composing a path is enough to run it, and to run
|
||||
it as whoever it runs as: `validate_operator_composed_flow` re-checks each path under the
|
||||
caller's RLS. This is the general case; the one below is on top of it, not instead of it.
|
||||
- **the `(path, hash)` of every version-pinned step**. A step carrying a `hash` is dispatched by
|
||||
that hash alone, with the path beside it never consulted, so a readable path paired with another
|
||||
script's hash still runs that other script.
|
||||
|
||||
Call it on every write **and** every preview: `run_preview_flow_job` and
|
||||
`push_flow_dependencies_job` both take a request-supplied flow value, so leaving either out makes
|
||||
it the way to run what the write path refuses.
|
||||
|
||||
## Billing
|
||||
|
||||
An operator of a builder workspace consumes a full author seat: composing deployable artifacts
|
||||
makes them an author, and there is no half-author. `consumes_operator_seat` is the seat-role
|
||||
helper; the EE counting queries share `OPERATOR_SEAT_SQL` so the displayed, enforced and reported
|
||||
numbers agree.
|
||||
|
||||
Granting the right runs `check_seat_cap_for_operator_builder`, which prices the change by counting
|
||||
seats twice rather than by counting the workspace's operators: an operator who already authors
|
||||
elsewhere must not be charged again, so re-saving settings that already have the right on is a
|
||||
zero delta and never blocks.
|
||||
|
||||
## Accepted risks
|
||||
|
||||
- All-or-nothing per workspace: there is no per-user builder role.
|
||||
- `operator_settings` is git-synced, so a pull can flip every operator's class in a workspace and
|
||||
the billed seat count with it.
|
||||
Reference in New Issue
Block a user