mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-27 08:02:39 +00:00
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:
co-authored by
Claude Opus 5.5
parent
da443d2fb2
commit
e5dedd147e
@@ -16,8 +16,8 @@ use axum::{
|
||||
};
|
||||
use windmill_api_auth::{
|
||||
auth::{list_tokens_internal, TruncatedTokenWithEmail},
|
||||
build_scope_path_predicate, check_scopes, get_scope_tags, maybe_refresh_folders,
|
||||
require_owner_of_path, ApiAuthed,
|
||||
build_scope_path_predicate, check_scopes, maybe_refresh_folders, require_owner_of_path,
|
||||
ApiAuthed,
|
||||
};
|
||||
use windmill_common::roles::AuthorLevel;
|
||||
use windmill_common::workspaces::{
|
||||
@@ -643,85 +643,11 @@ pub async fn validate_operator_composed_flow(
|
||||
user_db: &UserDB,
|
||||
w_id: &str,
|
||||
) -> error::Result<()> {
|
||||
let mut refs = windmill_common::flows::check_flow_is_composition_only(value)?;
|
||||
|
||||
// A tag is how a step picks the worker group it runs on: unauthorized, a builder could route
|
||||
// a job onto a privileged one.
|
||||
refs.tags.extend(flow_tag.clone().filter(|t| !t.is_empty()));
|
||||
if !refs.tags.is_empty() {
|
||||
// Job-aware: a WM_TOKEN running as a superadmin must not unlock restricted tags.
|
||||
let is_super_admin = windmill_api_auth::is_super_admin_authed(db, authed).await?;
|
||||
for tag in &refs.tags {
|
||||
windmill_common::jobs::check_tag_available_for_workspace_internal(
|
||||
db,
|
||||
w_id,
|
||||
tag,
|
||||
None,
|
||||
std::future::ready(w_id.to_string()),
|
||||
is_super_admin,
|
||||
get_scope_tags(authed),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
if refs.runnables.is_empty() && refs.pinned_scripts.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
// A flow can step through the same script thirty times; this runs on every write, preview and
|
||||
// dependency job.
|
||||
refs.runnables.sort();
|
||||
refs.runnables.dedup();
|
||||
refs.pinned_scripts
|
||||
.sort_by_key(|(path, hash)| (path.clone(), hash.0));
|
||||
refs.pinned_scripts
|
||||
.dedup_by_key(|(path, hash)| (path.clone(), hash.0));
|
||||
// The worker resolves a step's path with the root DB handle and runs it as that runnable's
|
||||
// `on_behalf_of`, so composing an unreadable path would run code the builder cannot see. RLS
|
||||
// on this transaction is the check.
|
||||
let mut tx = user_db.clone().begin(authed).await?;
|
||||
for (is_flow, path) in &refs.runnables {
|
||||
let readable = if *is_flow {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM flow WHERE workspace_id = $1 AND path = $2)",
|
||||
w_id,
|
||||
path,
|
||||
)
|
||||
} else {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM script WHERE workspace_id = $1 AND path = $2)",
|
||||
w_id,
|
||||
path,
|
||||
)
|
||||
}
|
||||
.fetch_one(&mut *tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
if !readable {
|
||||
return Err(Error::PermissionDenied(format!(
|
||||
"{} {path} does not exist or is not readable by you",
|
||||
if *is_flow { "Flow" } else { "Script" }
|
||||
)));
|
||||
}
|
||||
}
|
||||
// A pinned step is dispatched by its hash alone, ignoring the path beside it, so a readable
|
||||
// path paired with another script's hash would still run that other script.
|
||||
for (path, hash) in &refs.pinned_scripts {
|
||||
let exists = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM script WHERE workspace_id = $1 AND path = $2 AND hash = $3)",
|
||||
w_id,
|
||||
path,
|
||||
hash.0,
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
if !exists {
|
||||
return Err(Error::PermissionDenied(format!(
|
||||
"Version {hash} is not a readable version of {path}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
windmill_api_auth::composed::authorize_composed_flow(
|
||||
value, flow_tag, authed, db, &mut tx, w_id,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user