mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
A trigger run or an agent job has nobody to approve a step, and so far the approval mode auto_readonly and the act.* grants were only recorded. The prompt was the only thing that kept such a run from acting. Every tool call of an unattended run now goes through one check at the call. A tool that only reads runs. Any other tool needs the grant of its class, and the check fails closed: - act.files for write_file, edit_file, move_file, delete_file and the built-in bash; - act.addons, or act.addons:<id>, for add-on tools whose declared kind is not read, plus act.spend when the add-on gates the tool with a permission; - act.spend for MCP tools. A trigger holds its new acts list within the grants of its agent. A trigger that declared read only runs, or that a binding in auto_readonly mode made without asking, is refused every non-read tool whatever it holds. A binding that cannot be read counts as auto_readonly. A job holds the act grants of its agent. Sub-agents follow their parent, and a saved agent cannot be started from an unattended run. A refused call answers with a plain sentence the model can pass on, not an error, and writes one audit log line. Triggers set acts through the REST API; a model never can. A trigger whose plan names a file it writes keeps act.files. Assisted-by: Claude:claude-sonnet-5-5 go ginkgo swag
134 lines
4.7 KiB
Go
134 lines
4.7 KiB
Go
package studio
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"github.com/mudler/LocalAI/core/services/studio/mcpconns"
|
|
"github.com/mudler/LocalAI/core/services/studio/session"
|
|
"github.com/mudler/LocalAI/core/services/studio/workspace"
|
|
)
|
|
|
|
// reservedToolNames are the names the service owns. A tool from another source
|
|
// cannot take one, so the model always gets the real thing.
|
|
var reservedToolNames = map[string]bool{"spawn_agent": true, proposeTriggerName: true, listTriggersName: true,
|
|
webSearchName: true, webFetchName: true, listAgentsName: true}
|
|
|
|
// hostTools returns the tools a conversation has besides the ones the service
|
|
// adds itself: those from Deps.HostTools, the add-on tools of its user, the
|
|
// collection tools and the MCP tools of its model. A chat with a saved agent
|
|
// gets only what the agent grants. The second result lists the grants that
|
|
// could not be used, as phrases for a note to the model.
|
|
func (s *Service) hostTools(ctx context.Context, a workspace.Actor) ([]session.Tool, error) {
|
|
tools, _, err := s.scopedHostTools(ctx, a)
|
|
return tools, err
|
|
}
|
|
|
|
func (s *Service) scopedHostTools(ctx context.Context, a workspace.Actor) ([]session.Tool, []string, error) {
|
|
rec, err := s.d.Store.GetChat(ctx, a.Principal, a.ChatID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
sc := s.scopeOf(ctx, rec)
|
|
var tools []session.Tool
|
|
if s.d.HostTools != nil {
|
|
ts, err := s.d.HostTools(ctx, a)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
tools = append(tools, ts...)
|
|
}
|
|
if sc != nil && sc.deleted {
|
|
return tools, []string{"the agent of this chat was deleted"}, nil
|
|
}
|
|
// The chat's switches of the built-in groups also take away the add-on
|
|
// tool that replaces bash, and the collection tools.
|
|
hidden := s.hiddenFor(ctx, rec)
|
|
var missing []string
|
|
t, m, err := s.addonTools(ctx, a, rec, sc)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
tools, missing = append(tools, dropHidden(t, hidden)...), append(missing, m...)
|
|
if !hidden[collectionToolNames[0]] {
|
|
t, m = s.collectionTools(ctx, a, sc)
|
|
tools, missing = append(tools, t...), append(missing, m...)
|
|
}
|
|
t, m = s.mcpTools(ctx, a, rec, sc, tools)
|
|
// MCP tools are external: an unattended run needs act.spend for them.
|
|
t = s.gateExternalAct(ctx, rec, t)
|
|
return append(tools, t...), append(missing, m...), nil
|
|
}
|
|
|
|
// mcpTools gives the chat its MCP tools: those of the model's own servers and
|
|
// those of the user's connectors, less what the chat switched off. A chat with
|
|
// a saved agent gets only the servers and connectors the agent names; a name
|
|
// with the prefix "connector:" is a connector, any other is a server of the
|
|
// model. A server that cannot be reached gives no tools and a warning; it never
|
|
// fails the chat. have lists the tools the chat has already, so that a name
|
|
// is never taken twice.
|
|
func (s *Service) mcpTools(ctx context.Context, a workspace.Actor, rec session.ChatRecord, sc *scope, have []session.Tool) ([]session.Tool, []string) {
|
|
off := set(rec.McpOff)
|
|
var servers, refs []string
|
|
if sc != nil {
|
|
for _, n := range sc.mcp {
|
|
if off[n] {
|
|
continue
|
|
}
|
|
if id, ok := strings.CutPrefix(n, mcpconns.Prefix); ok {
|
|
refs = append(refs, id)
|
|
} else {
|
|
servers = append(servers, n)
|
|
}
|
|
}
|
|
}
|
|
tools, missing := s.modelMCPTools(ctx, a, rec, sc, servers, off)
|
|
ct, cm := s.connectorTools(ctx, a, rec, sc, refs, off, append(append([]session.Tool(nil), have...), tools...))
|
|
return append(tools, ct...), append(missing, cm...)
|
|
}
|
|
|
|
// modelMCPTools turns the MCP tools of the conversation's model into chat
|
|
// tools. The executor is built each time: it reuses cached MCP sessions.
|
|
func (s *Service) modelMCPTools(ctx context.Context, a workspace.Actor, rec session.ChatRecord, sc *scope, servers []string, off map[string]bool) ([]session.Tool, []string) {
|
|
if s.d.MCP == nil {
|
|
return nil, nil
|
|
}
|
|
var only []string
|
|
limited := sc != nil
|
|
if sc != nil {
|
|
if len(servers) == 0 {
|
|
return nil, nil
|
|
}
|
|
only = servers
|
|
} else if s.d.MCPServers != nil && len(off) > 0 {
|
|
// A chat with no agent uses the servers the model picks by itself. To
|
|
// leave some out, ask for the others by name.
|
|
var auto, kept []string
|
|
for _, m := range s.d.MCPServers(ctx, a.Principal, rec.Model) {
|
|
if m.Auto {
|
|
auto = append(auto, m.Name)
|
|
if !off[m.Name] {
|
|
kept = append(kept, m.Name)
|
|
}
|
|
}
|
|
}
|
|
if len(kept) < len(auto) {
|
|
if len(kept) == 0 {
|
|
return nil, nil
|
|
}
|
|
only, limited = kept, true
|
|
}
|
|
}
|
|
exec, absent := s.d.MCP(ctx, a.Principal, rec.Model, only, limited)
|
|
missing := prefixed("MCP server", absent)
|
|
if exec == nil || !exec.HasTools() {
|
|
return nil, missing
|
|
}
|
|
tools, err := s.toolsOfApps(ctx, a.ChatID, exec, true)
|
|
if err != nil {
|
|
s.log.Warn("studio: MCP tools unavailable", "chat", a.ChatID, "model", rec.Model, "error", err)
|
|
return nil, missing
|
|
}
|
|
return tools, missing
|
|
}
|