Files
LocalAI/core/config/addon_host_ops.go
T
Ettore Di Giacinto 082fd7ee7d feat(addons): add schemaVersion 2 host policy with a deny list
A policy file may now carry a deny object. Deny rules name an add-on ID
or "*" and list prefixes, exact operation names or the single token "*".
Schema 1 files parse as before; a deny key in a schema 1 file, or a
version above 2, is an error that names the problem.

The selected file is now always opened, even when a flat
--addon-host-ops list replaces its grants, because a deny must not
vanish when someone adds a flag. An invalid selected file still stops
startup.

Add --addon-host-deny (LOCALAI_ADDON_HOST_DENY), unioned with the file,
and the --addon-host-blanket and --addon-host-ui-grants flags with their
config options.

Assisted-by: Claude:claude-sonnet-5-5 go
2026-11-03 21:22:56 +00:00

134 lines
4.5 KiB
Go

// SPDX-License-Identifier: MIT
package config
import (
"fmt"
"regexp"
"slices"
"strings"
)
var (
addonIDForHostOps = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
hostOpPrefix = regexp.MustCompile(`^[a-z]+\.([a-z]+\.)*$`)
hostOpExact = regexp.MustCompile(`^[a-z]+(\.[a-z]+)+$`)
)
// ParseAddonHostOps reads a comma list of addon-id=op-prefix pairs. An add-on
// may appear more than once. An empty string names nobody.
func ParseAddonHostOps(s string) (map[string][]string, error) {
out := map[string][]string{}
for _, item := range strings.Split(s, ",") {
item = strings.TrimSpace(item)
if item == "" {
continue
}
id, prefix, ok := strings.Cut(item, "=")
id, prefix = strings.TrimSpace(id), strings.TrimSpace(prefix)
if !ok || !addonIDForHostOps.MatchString(id) || !hostOpPrefix.MatchString(prefix) {
return nil, fmt.Errorf("invalid host operation entry %q: want addon-id=op-prefix, such as custom-addon=workspace.", item)
}
out[id] = append(out[id], prefix)
}
return out, nil
}
// WithAddonHostOps sets which add-ons may call which host operations.
// Nil and empty maps grant nothing. The run command resolves explicit host
// policy before passing its startup snapshot here.
func WithAddonHostOps(ops map[string][]string) AppOption {
snapshot := CloneAddonHostOps(ops)
return func(o *ApplicationConfig) { o.AddonHostOps = CloneAddonHostOps(snapshot) }
}
// validDenyEntry accepts what a deny rule may name: a prefix (workspace.),
// one exact operation (workspace.admin) or the single token "*".
func validDenyEntry(s string) bool {
return s == "*" || hostOpPrefix.MatchString(s) || hostOpExact.MatchString(s)
}
// ParseAddonHostDeny reads a comma list of addon-id=entry pairs, where addon-id
// may be "*" for every add-on and entry is a prefix, an exact operation name
// or "*". Duplicates are errors; "*" must be the only entry of its add-on.
func ParseAddonHostDeny(s string) (map[string][]string, error) {
out := map[string][]string{}
for _, item := range strings.Split(s, ",") {
item = strings.TrimSpace(item)
if item == "" {
continue
}
id, entry, ok := strings.Cut(item, "=")
id, entry = strings.TrimSpace(id), strings.TrimSpace(entry)
if !ok || !(id == "*" || addonIDForHostOps.MatchString(id)) || !validDenyEntry(entry) {
return nil, fmt.Errorf("invalid host deny entry %q: want addon-id=op-prefix or *=op-prefix, such as *=egress. or custom-addon=*", item)
}
for _, existing := range out[id] {
if existing == entry {
return nil, fmt.Errorf("duplicate host deny entry %q", item)
}
}
out[id] = append(out[id], entry)
}
if len(out) > 128 {
return nil, fmt.Errorf("host deny exceeds 128 addons")
}
for id, entries := range out {
if len(entries) > 32 {
return nil, fmt.Errorf("host deny exceeds 32 entries for %q", id)
}
for _, e := range entries {
if e == "*" && len(entries) != 1 {
return nil, fmt.Errorf(`host deny "*" must be the only entry of %q`, id)
}
}
}
return out, nil
}
// MergeAddonHostDeny unions deny maps. Entries stay in first-seen order and a
// "*" absorbs every other entry of its key. The result is never nil.
func MergeAddonHostDeny(maps ...map[string][]string) map[string][]string {
out := map[string][]string{}
for _, m := range maps {
for id, entries := range m {
for _, e := range entries {
if !slices.Contains(out[id], e) {
out[id] = append(out[id], e)
}
}
}
}
for id, entries := range out {
if slices.Contains(entries, "*") {
out[id] = []string{"*"}
}
}
return out
}
// ParseAddonHostBlanket validates the deployment blanket mode. The empty
// string means the deployment does not set it and the UI decides.
func ParseAddonHostBlanket(s string) (string, error) {
switch s = strings.TrimSpace(s); s {
case "", "off", "official", "any":
return s, nil
}
return "", fmt.Errorf("invalid --addon-host-blanket %q: want off, official or any", s)
}
// WithAddonHostDeny sets the startup deny rules. The map is copied.
func WithAddonHostDeny(deny map[string][]string) AppOption {
snapshot := CloneAddonHostOps(deny)
return func(o *ApplicationConfig) { o.AddonHostDeny = CloneAddonHostOps(snapshot) }
}
// WithAddonHostBlanket sets the deployment blanket mode ("" leaves it to the UI).
func WithAddonHostBlanket(mode string) AppOption {
return func(o *ApplicationConfig) { o.AddonHostBlanket = mode }
}
// WithAddonHostUIGrantsLocked makes the UI grant store read-only.
func WithAddonHostUIGrantsLocked(locked bool) AppOption {
return func(o *ApplicationConfig) { o.AddonHostUIGrantsLocked = locked }
}