feat: let flow builder operators build AI agents (#11898)

* feat: let flow builder operators build AI agents

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: check linked agents' bound tools and type, validate on the write's transaction

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: bound the linked-agent walk by sorting bound tool ids

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: let builders bind only the agent tool inputs left to the model

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: check every agent tool sharing a bound id

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: show builders the agent's own tool inputs read-only and name agents in the setting

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: show the agent's own tool inputs with the read-only step viewer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* style: give the agent-set inputs heading the input label style

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* style: label the agent's fixed tool inputs static inputs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: address cubic review on agent builder rights

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: check agent writes against the role's flows capability

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: name the flows capability as the agent builder right

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: gate agent evals on the role's code capability

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: move the composed-flow check into windmill-api-auth

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:
hugocasa
2026-10-23 15:05:20 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent da443d2fb2
commit e5dedd147e
27 changed files with 763 additions and 167 deletions
+31 -6
View File
@@ -1,8 +1,8 @@
# Operator builder rights
Two workspace settings, `operator_settings.builder_flows` and `operator_settings.builder_apps`,
that let every operator of that workspace compose flows and full-code apps out of runnables that
already exist. Neither lets them author backend code or run arbitrary code on a worker, the
that let every operator of that workspace compose flows (and AI agents, under the flow right) and
full-code apps out of runnables that already exist. Neither lets them author backend code or run arbitrary code on a worker, the
boundary the operator role draws. The app right does let them write the app's frontend code,
which runs in the viewer's browser and is why that right rests on sandbox isolation.
@@ -32,13 +32,24 @@ anything that carries code. Three of its rules exist because the obvious walk mi
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:
- **every agent a step links**. A linked agent resolves its tools from its `ai_agent` resource at
run time, outside this walk, so the link is allowed only to an agent the builder can read. That
is safe because of the agent rule below: an agent's tools are either a developer's code or
passed this same check when an operator saved them. Two things keep it safe:
- The step's `tool_inputs` replace a tool's inputs at run time, including the ones the agent's
author set. A builder's step may bind only the inputs the agent leaves open
(`check_bound_agent_inputs_are_open`, on `InputTransform::is_completed`), and the flow editor
lets a builder edit only those, showing the rest read-only. What it binds there, a `$var:` or
a JavaScript expression included, is what its own steps may already pass a deployed runnable; an input the author set
stays theirs. The check runs on the flow's write, so a tool or input the agent gains later is
open to a binding written before it, like the retyped path below.
- The worker refuses a linked path whose resource is no longer an `ai_agent`
(`get_agent_value`). Only writes of an agent are checked, so a path retyped, or recreated as
another type, after the link was approved would otherwise run whatever `tools` it now holds.
- **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
@@ -61,6 +72,20 @@ Call it on every write **and** every preview: `run_preview_flow_job` and
it the way to run what the write path refuses. A flow draft goes through `validate_operator_flow`
too: a developer who loads a builder's draft in the editor runs its code as themselves.
## Agents
An agent is an `ai_agent` resource, and operators may write resources, so the `flows` capability
is also the right to build agents. Without it a restricted role writes no agent at all. Its tools run in the
agent's own runs, which take only inputs and run what is stored, and in every flow step linking
it. So `validate_operator_agent` holds an operator's agent to the composed-flow rules by checking
its tools as the tools of a one-step flow. It is called from `resolve_agent_on_behalf_of`, which
every write of an agent's value passes through: create, update, retype, update-value and version
restore. An operator's resource draft is checked the same way when it names `ai_agent` **or** an
agent is deployed at its path, whatever type the draft names: deploying a draft from the resource
editor keeps the row's type. A draft saved before an agent is created at its path is not
re-checked when someone else deploys it. A new route that writes an agent's value must go through
that function.
The same reasoning applies to a builder-authored app, with one extra step. `execute_component`
resolves the runnable it runs on the root handle, so `validate_operator_composed_app` checks every
referenced path under the caller's RLS and refuses hub ones. For a script it checks the version a
+1 -1
View File
@@ -16,7 +16,7 @@ code is `windmill_common::roles`.
| Capability | Values | Enforced |
|---|---|---|
| `code` | bool | yes: scripts, previews, dependency jobs, dynamic selects, resolving jobs, data table migrations, eval datasets |
| `flows` | `none` / `compose` / `full` | yes |
| `flows` | `none` / `compose` / `full` | yes; also AI agents, whose tools are checked as composed steps |
| `apps` | `none` / `compose` / `full` | yes; low-code apps and worker-built apps need `full` |
| `manage_schedules`, `manage_triggers` | bool | yes, see `docs/operator-write-rights.md` |
| `visibility.*` | bool per page | no, UI only |