mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-28 09:08:54 -04:00
feat(backends): --disable-backend-install refuses every backend acquisition A hardening switch for deployments that must run exactly the backend set they were tested with (production, air-gapped, immutable images, package-managed hosts). With LOCALAI_DISABLE_BACKEND_INSTALL=1 every install and upgrade path refuses — gallery, OCI/URL/path, node-scoped, auto-upgrade, boot-time external backends — HTTP routes answer 403 up front, and a model whose backend is missing is refused before any download: 409 on the API, an error on preload and import. Whether a backend is present is asked of the execution target: the local backend directory on a single node, the workers' listings on a distributed frontend, which installs nothing itself. Backends already present, including system-path ones, keep working. Assisted-by: Claude Code:claude-fable-5 Signed-off-by: Plamen K. Kosseff <p.kosseff@gmail.com>
129 lines
3.7 KiB
Go
129 lines
3.7 KiB
Go
package gallery
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"github.com/mudler/LocalAI/core/config"
|
|
"github.com/mudler/LocalAI/pkg/modelartifacts"
|
|
)
|
|
|
|
type ArtifactMaterializer interface {
|
|
Ensure(context.Context, string, modelartifacts.Spec) (modelartifacts.Result, error)
|
|
}
|
|
|
|
type installOptions struct {
|
|
materializer ArtifactMaterializer
|
|
variant string
|
|
backendAvailability BackendAvailability
|
|
}
|
|
|
|
type InstallOption func(*installOptions)
|
|
|
|
// WithBackendAvailability supplies the execution target's view of installed
|
|
// backends; absent, the local host's.
|
|
func WithBackendAvailability(available BackendAvailability) InstallOption {
|
|
return func(options *installOptions) {
|
|
if available != nil {
|
|
options.backendAvailability = available
|
|
}
|
|
}
|
|
}
|
|
|
|
func WithArtifactMaterializer(materializer ArtifactMaterializer) InstallOption {
|
|
return func(options *installOptions) {
|
|
if materializer != nil {
|
|
options.materializer = materializer
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithVariant pins a gallery entry to a specific variant by name, bypassing
|
|
// hardware-based selection. The entry's own name is a valid pin, since the
|
|
// entry is itself the last resort. Ignored for entries that declare no
|
|
// variants.
|
|
func WithVariant(variant string) InstallOption {
|
|
return func(options *installOptions) {
|
|
options.variant = variant
|
|
}
|
|
}
|
|
|
|
func applyInstallOptions(options ...InstallOption) installOptions {
|
|
result := installOptions{materializer: modelartifacts.NewDefaultManager()}
|
|
for _, option := range options {
|
|
option(&result)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func bindPrimaryArtifact(ctx context.Context, modelsPath string, typed *config.ModelConfig, configMap map[string]any, materializer ArtifactMaterializer, artifactSpec modelartifacts.Spec, inferred bool) (bool, error) {
|
|
result, err := materializer.Ensure(ctx, modelsPath, artifactSpec)
|
|
if err != nil {
|
|
if inferred {
|
|
config.LogArtifactFallback(typed.Name, typed.Backend, err)
|
|
return false, nil
|
|
}
|
|
return false, fmt.Errorf("materialize primary model artifact: %w", err)
|
|
}
|
|
next := []modelartifacts.Spec{result.Spec}
|
|
if len(typed.Artifacts) > 1 {
|
|
next = append(next, typed.Artifacts[1:]...)
|
|
}
|
|
typed.Artifacts = next
|
|
// Companions are always explicit, so there is no legacy path to degrade to:
|
|
// failing here leaves any previously installed config untouched rather than
|
|
// persisting a half-acquired model that would fail later inside the backend.
|
|
for i := 1; i < len(typed.Artifacts); i++ {
|
|
if typed.Artifacts[i].Target != modelartifacts.TargetCompanion {
|
|
continue
|
|
}
|
|
companion, err := materializer.Ensure(ctx, modelsPath, typed.Artifacts[i])
|
|
if err != nil {
|
|
return false, fmt.Errorf("materialize companion artifact %q: %w", typed.Artifacts[i].Name, err)
|
|
}
|
|
typed.Artifacts[i] = companion.Spec
|
|
}
|
|
artifactYAML, err := yaml.Marshal(typed.Artifacts)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
var artifactValue any
|
|
if err := yaml.Unmarshal(artifactYAML, &artifactValue); err != nil {
|
|
return false, err
|
|
}
|
|
configMap["artifacts"] = artifactValue
|
|
return true, nil
|
|
}
|
|
|
|
func writeModelConfigAtomic(fileName string, data []byte) error {
|
|
temporary, err := os.CreateTemp(filepath.Dir(fileName), ".model-config-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
temporaryName := temporary.Name()
|
|
defer func() { _ = os.Remove(temporaryName) }()
|
|
if err := temporary.Chmod(0600); err != nil {
|
|
_ = temporary.Close()
|
|
return err
|
|
}
|
|
if _, err := temporary.Write(data); err != nil {
|
|
_ = temporary.Close()
|
|
return err
|
|
}
|
|
if err := temporary.Sync(); err != nil {
|
|
_ = temporary.Close()
|
|
return err
|
|
}
|
|
if err := temporary.Close(); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Chmod(temporaryName, 0644); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(temporaryName, fileName)
|
|
}
|