Files
LocalAI/core/services/studio/agentscope.go
T
Ettore Di Giacinto 24c837e453 feat(studio): grant built-in capabilities to a saved agent
A saved agent now has a Builtins list: the capabilities its owner gave
it (memory.write, profile.write, skills.write, pages.write,
dashboard.write, triggers.propose, triggers.run, talkback.channels,
talkback.new_channel, files.upload, act.files, act.addons, act.send,
act.spend and the opt-in group saved_agents). The list is empty by
default, and nothing turns a grant on for a template, an import or a
write that omits the field.

A chat of the agent holds the grants the user may use at the turn
(agent scope). A tool exists when the grant is held, the chat did not
switch its group off and the run can hold rights: a sub-agent or a run
that started by itself never does. propose_change and revert_change
now exist only for a chat that holds the grant of at least one kind,
and the schema lists only those kinds.

propose_change gains the kinds skill, page and dashboard. A skill is a
SKILL.md for the personal or the agent layer, and a skill with
locked: true is refused, also as a shadow of a locked personal skill.
A page is a set of files checked against the Pages rules and written
as one change set. The dashboard is a home.json checked against its
schema. All three are re-checked when the owner confirms or edits.

GET /api/chats/agents/builtin-grants lists each grant with its title,
description and sensitive flag. The agent record, the write body and
the export file carry builtins; an old file imports as before and an
unknown name is dropped in an import and refused in a write.

Assisted-by: Claude:claude-sonnet-5-5 go gofmt swag
2026-11-03 14:56:42 +00:00

84 lines
2.3 KiB
Go

package studio
import (
"context"
"errors"
"sort"
"strings"
"github.com/mudler/LocalAI/core/services/studio/agents"
"github.com/mudler/LocalAI/core/services/studio/session"
)
// scope is what a saved agent allows a chat. It only narrows: each list is
// intersected with what the user may use at the time of the turn. A nil scope is
// a plain chat, which gets everything its user may use.
type scope struct {
instructions string
deleted bool // the chat names an agent that no longer exists
mcp []string
addons map[string]bool
collections map[string]bool
skills map[string]bool
builtins map[string]bool // the builtin grants the agent holds now
builtinNames []string
skillNames []string
addonNames []string
collNames []string
}
func set(l []string) map[string]bool {
m := make(map[string]bool, len(l))
for _, v := range l {
m[v] = true
}
return m
}
// scopeOf returns the scope of a chat, or nil for a plain chat.
func (s *Service) scopeOf(ctx context.Context, rec session.ChatRecord) *scope {
if rec.AgentID == "" {
return nil
}
if s.d.Agents == nil {
return &scope{deleted: true}
}
ag, err := s.d.Agents.Get(ctx, rec.Principal, rec.AgentID)
if errors.Is(err, agents.ErrNotFound) {
return &scope{deleted: true}
}
if err != nil {
s.log.Warn("studio: agent unavailable", "chat", rec.ID, "agent", rec.AgentID, "error", err)
return &scope{deleted: true}
}
held := s.usableGrants(ctx, rec.Principal, ag.Builtins)
return &scope{
builtins: set(held), builtinNames: held,
instructions: strings.TrimSpace(ag.Instructions), mcp: ag.MCPServers,
addons: set(ag.Addons), collections: set(ag.Collections), skills: set(ag.Skills),
addonNames: ag.Addons, collNames: ag.Collections, skillNames: ag.Skills,
}
}
// unavailable renders the grants that could not be used into one line for the
// model. Empty when nothing is missing.
func unavailable(parts ...[]string) string {
var all []string
for _, p := range parts {
all = append(all, p...)
}
if len(all) == 0 {
return ""
}
sort.Strings(all)
return "unavailable this turn: " + strings.Join(all, ", ")
}
func prefixed(prefix string, names []string) []string {
out := make([]string, len(names))
for i, n := range names {
out[i] = prefix + " " + n
}
return out
}