Files
LocalAI/core/services/modeladmin/state.go
T
mudler-agentandEttore Di Giacinto 50c284fdcc fix: make the remaining VerifyPath checks effective (#12326)
utils.VerifyPath joins its argument onto the base path, so a path that
the caller already joined always passes. Several callers gave it joined
paths, and their checks could not fail:

- modeladmin (config view, patch, edit, pin and state): the config file
  path from the loader. A config loaded from outside the models
  directory (--models-config-file) could be pinned, and the pin wrote
  the outside file. The patch and state paths stopped later, in the
  mutation snapshot, with a different error.
- core/backend/tts.go: the model path joined onto the models path.
- The trellis2cpp and stablediffusion-ggml backends: option paths
  (*_path) joined onto the model path. A "../" value outside the model
  directory was accepted.

Add utils.VerifyResolvedPath for a full path. modeladmin and tts use
it. The backends now check the relative option value before they join
it. A rename in modeladmin checks the new relative name.

For models from a config file outside the models directory, the admin
API and web UI now return ErrPathNotTrusted for view, edit, pin, and
enable or disable. The docs describe this.

Assisted-by: Claude:claude-opus-5-5 [Claude Code]

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-09-28 10:04:22 +02:00

114 lines
3.5 KiB
Go

package modeladmin
import (
"context"
"fmt"
"os"
"gopkg.in/yaml.v3"
"github.com/mudler/LocalAI/pkg/utils"
)
// ToggleResult is shared by ToggleState and TogglePinned.
type ToggleResult struct {
Filename string
Action Action
ConfigRevision string
PendingCleanup int
}
// ToggleState enables or disables an installed model. action must be
// ActionEnable or ActionDisable. The revision lifecycle quarantines existing
// replicas before cleanup when the state changes.
//
// The on-disk YAML is mutated as a generic map so unrelated fields are
// preserved verbatim; we only set or remove the `disabled` key.
func (s *ConfigService) ToggleState(ctx context.Context, name string, action Action) (*ToggleResult, error) {
var result *ToggleResult
err := s.Loader.WithModelConfigMutation(func() error {
var err error
result, err = s.toggleState(ctx, name, action)
return err
})
return result, err
}
func (s *ConfigService) toggleState(ctx context.Context, name string, action Action) (*ToggleResult, error) {
if name == "" {
return nil, ErrNameRequired
}
if !action.Valid(ActionEnable, ActionDisable) {
return nil, fmt.Errorf("%w: must be %q or %q, got %q", ErrBadAction, ActionEnable, ActionDisable, action)
}
cfg, exists := s.Loader.GetModelConfig(name)
if !exists {
return nil, ErrNotFound
}
configPath := cfg.GetModelConfigFile()
if configPath == "" {
return nil, ErrConfigFileMissing
}
if err := utils.VerifyResolvedPath(configPath, s.modelsPath()); err != nil {
return nil, fmt.Errorf("%w: %v", ErrPathNotTrusted, err)
}
var result *ToggleResult
err := s.withMutationRollback([]string{configPath}, func() error {
if err := mutateYAMLBoolFlag(configPath, "disabled", action == ActionDisable); err != nil {
return err
}
if err := s.Loader.LoadModelConfigsFromPath(s.modelsPath(), s.AppConfig.ToConfigLoaderOptions()...); err != nil {
return fmt.Errorf("reload configs: %w", err)
}
if _, ok := s.Loader.GetModelConfig(name); !ok {
return fmt.Errorf("reload configs: model %q missing", name)
}
// Resolve the revision the way an inference request does. Hashing the
// stored config instead publishes a value no request will ever carry,
// because SetDefaults runs again on the request path and is not
// idempotent for every model, and the edit would leave the model
// unroutable.
revision, err := s.Loader.RevisionFor(name, s.AppConfig)
if err != nil {
return err
}
pending, err := s.applyRevision(ctx, name, name, revision, action == ActionDisable)
if err != nil {
return err
}
result = &ToggleResult{Filename: configPath, Action: action, ConfigRevision: revision, PendingCleanup: pending}
return nil
})
return result, err
}
// mutateYAMLBoolFlag is a small helper shared by ToggleState and
// TogglePinned: read the file as a generic map, set or remove a bool key,
// write back. Setting `set=false` removes the key for a clean YAML.
func mutateYAMLBoolFlag(path, key string, set bool) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read config: %w", err)
}
var m map[string]any
if err := yaml.Unmarshal(data, &m); err != nil {
return fmt.Errorf("parse config: %w", err)
}
if m == nil {
m = map[string]any{}
}
if set {
m[key] = true
} else {
delete(m, key)
}
out, err := yaml.Marshal(m)
if err != nil {
return fmt.Errorf("marshal config: %w", err)
}
if err := writeFileAtomic(path, out, 0644); err != nil {
return fmt.Errorf("write config: %w", err)
}
return nil
}