mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
A trigger run with nobody present only reads unless its trigger holds act.* grants, and until now only the REST API could set them. An agent that proposes a trigger can now ask for acts too: - propose_trigger takes an optional acts list. The names must be act.* grants the agent of the chat holds; anything else is refused and nothing is stored. A read_only proposal cannot carry acts. - The proposal stores the list. Confirming it, in Studio, by a plain yes or by confirm CODE, grants exactly that list. No tool changes the acts of a trigger afterwards. Two proposals that differ only in their acts are kept apart. - The text of a proposal in a messaging app now has a Permissions line, in plain words with sensitive classes marked: read only, or what a run may do. The confirmation repeats it. A proposal that asks for acts is never set up without a yes, whatever the approval mode. - The trigger REST object gains acts_held, read_only and read_only_why, computed with the same rules as the run, so a client can show why a run only reads. Assisted-by: Claude:claude-sonnet-5-5 go ginkgo
149 lines
4.6 KiB
Go
149 lines
4.6 KiB
Go
package studio
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/mudler/LocalAI/core/services/studio/builtins"
|
|
"github.com/mudler/LocalAI/core/services/studio/triggers"
|
|
)
|
|
|
|
// Why the runs of a trigger only read.
|
|
const (
|
|
// ReadOnlyDeclared: the proposal said that its runs only read and report.
|
|
ReadOnlyDeclared = "declared"
|
|
// ReadOnlyBinding: a connection in the auto_readonly mode made it.
|
|
ReadOnlyBinding = "binding"
|
|
// ReadOnlyNoActs: the trigger holds no act.* permission.
|
|
ReadOnlyNoActs = "no_acts"
|
|
// ReadOnlyAgent: the trigger holds permissions, but its agent holds none
|
|
// of them, so none applies.
|
|
ReadOnlyAgent = "agent"
|
|
)
|
|
|
|
// ActsView is what the runs of a trigger may do, as the interface shows it.
|
|
// It is computed with the same rules as the run (triggerPolicy).
|
|
type ActsView struct {
|
|
// Held are the act.* names that apply to a run: the trigger's own, within
|
|
// the grants of its agent, plus act.files when its plan names a file it
|
|
// writes. Empty for a read only run.
|
|
Held []string
|
|
// ReadOnly is true when a run may only read.
|
|
ReadOnly bool
|
|
// Why is a ReadOnly* code when ReadOnly is true.
|
|
Why string
|
|
}
|
|
|
|
// ActsView gives the rights of the runs of t.
|
|
func (h *Triggers) ActsView(ctx context.Context, t triggers.Trigger) ActsView {
|
|
ceiling := actNames(h.svc.AgentGrants(ctx, t.Owner, t.AgentID))
|
|
pol := h.svc.triggerPolicy(ctx, t, ceiling, t.AgentID != "")
|
|
v := ActsView{Held: pol.Held}
|
|
switch {
|
|
case t.Route.ReadOnly:
|
|
v.Why = ReadOnlyDeclared
|
|
case pol.ReadOnly:
|
|
v.Why = ReadOnlyBinding
|
|
case len(pol.Held) == 0 && len(t.Acts) == 0 && t.Writes == "":
|
|
v.Why = ReadOnlyNoActs
|
|
case len(pol.Held) == 0:
|
|
v.Why = ReadOnlyAgent
|
|
}
|
|
if v.Why != "" {
|
|
v.ReadOnly, v.Held = true, nil
|
|
}
|
|
return v
|
|
}
|
|
|
|
// checkProposedActs checks the acts a model asks for in a proposal. They must
|
|
// be valid names and lie within the act.* grants of the agent of the chat; a
|
|
// chat without an agent is the person's own and has no ceiling but the list.
|
|
// The model cannot name more than the agent holds, and nothing after the
|
|
// person confirms: no tool changes the acts of a trigger.
|
|
func checkProposedActs(names []string, agentID string, grants []string, readOnly bool) ([]string, error) {
|
|
acts, err := cleanActs(names)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(acts) == 0 {
|
|
return nil, nil
|
|
}
|
|
if readOnly {
|
|
return nil, bad("access read_only cannot come with acts; leave acts out, or use read_write")
|
|
}
|
|
if agentID == "" {
|
|
return acts, nil
|
|
}
|
|
ceiling := actNames(grants)
|
|
for _, n := range acts {
|
|
if !withinCeiling(n, ceiling) {
|
|
return nil, bad("your agent does not hold %s, so a trigger cannot get it. Leave it out; the owner can add it to the agent first", n)
|
|
}
|
|
}
|
|
return acts, nil
|
|
}
|
|
|
|
// sameActs says whether two lists name the same acts, in any order.
|
|
func sameActs(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for _, n := range a {
|
|
if !slices.Contains(b, n) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// actWords says in plain words what an act lets an unattended run do.
|
|
func actWords(name string) string {
|
|
switch name {
|
|
case builtins.ClassFiles:
|
|
return "write files in the agent's folders"
|
|
case builtins.ClassAddons:
|
|
return "use the actions of the agent's add-ons"
|
|
case builtins.ClassSend:
|
|
return "send messages beyond where it reports"
|
|
case builtins.ClassSpend:
|
|
return "use paid or external tools and the network"
|
|
}
|
|
if id, ok := strings.CutPrefix(name, builtins.ClassAddons+":"); ok {
|
|
return "use the actions of the add-on " + id
|
|
}
|
|
return name
|
|
}
|
|
|
|
// actSensitive says whether the act is marked sensitive in the grants list.
|
|
// An act of one add-on is sensitive like act.addons.
|
|
func actSensitive(name string) bool {
|
|
if strings.HasPrefix(name, builtins.ClassAddons+":") {
|
|
name = builtins.ClassAddons
|
|
}
|
|
g, ok := builtins.LookupGrant(name)
|
|
return ok && g.Sensitive
|
|
}
|
|
|
|
// actsText is one sentence about what the runs of a proposed trigger may do.
|
|
// It is on the text of a proposal in a chat of a messaging app, so the person
|
|
// reads it before they answer yes.
|
|
func actsText(t triggers.Trigger) string {
|
|
held := slices.Clone(t.Acts)
|
|
if t.Writes != "" && !slices.Contains(held, builtins.ClassFiles) {
|
|
held = append(held, builtins.ClassFiles)
|
|
}
|
|
if t.Route.ReadOnly || len(held) == 0 {
|
|
return "Permissions: read only. A run can read and report, and cannot change anything."
|
|
}
|
|
words := make([]string, len(held))
|
|
for i, n := range held {
|
|
words[i] = actWords(n)
|
|
if actSensitive(n) {
|
|
words[i] += " (sensitive)"
|
|
}
|
|
}
|
|
return fmt.Sprintf("Permissions: with nobody present, a run may %s. Say no if that is more than you want.", strings.Join(words, "; "))
|
|
}
|