feat(addons): default to local-owner when auth is off

A server without authentication has no accounts, so named-user left
the add-on owner off. Resolve the unset mode in one place: local-owner
when authentication is off, named-user when it is on. A flag or
environment value still wins, and an unknown value is still refused.

Assisted-by: Claude Code:claude-sonnet-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto committed 2026-10-20 21:40:01 +00:00
1 parent f75dcf2ce2
commit 898357d74c
5 files changed
+63 -13

No files matched your search

+3 -1
View File
@@ -35,7 +35,8 @@ import (
type RunCMD struct {
AddonHostOps string `name:"addon-host-ops" env:"LOCALAI_ADDON_HOST_OPS" default:"fine-tuning=finetune.,quantization=quantize." help:"Add-ons that may call host operations, as a comma list of addon-id=operation-prefix. An add-on that is not listed gets no host operation channel. Empty gives none" group:"addons"`
AddonsEnabled bool `name:"addons-enabled" env:"LOCALAI_ADDONS_ENABLED" default:"true" help:"Run the add-on owner, which installs and supervises add-ons. Turn it off with --addons-enabled=false" group:"addons"`
AddonsOwnershipMode string `name:"addons-ownership-mode" env:"LOCALAI_ADDONS_OWNERSHIP_MODE" default:"named-user" help:"Who may use add-ons: named-user (each account has its own access; needs authentication) or local-owner (every caller shares one owner)" group:"addons"`
AddonsOwnershipMode string `name:"addons-ownership-mode" env:"LOCALAI_ADDONS_OWNERSHIP_MODE" help:"Who may use add-ons: named-user (each account has its own access; needs authentication) or local-owner (every caller shares one owner). Default: local-owner when authentication is off, named-user when it is on" group:"addons"`
AddonsOfficialLibrary bool `name:"addons-official-library" env:"LOCALAI_ADDONS_OFFICIAL_LIBRARY" default:"true" help:"Register the official add-on library on the first start, when the add-on owner has no sources.json yet. A library the admin removes later stays removed. Turn it off with --addons-official-library=false" group:"addons"`
Pprof bool `name:"pprof" env:"LOCALAI_PPROF" default:"false" help:"Enable the private loopback profiling listener" group:"diagnostics"`
PprofAddress string `name:"pprof-address" env:"LOCALAI_PPROF_ADDRESS" default:"127.0.0.1:6060" help:"Numeric loopback address and port for profiling" group:"diagnostics"`
PprofMutexProfileFraction int `name:"pprof-mutex-profile-fraction" env:"LOCALAI_PPROF_MUTEX_PROFILE_FRACTION" default:"0" help:"Mutex sampling fraction (0 disables sampling); requires pprof" group:"diagnostics"`
@@ -662,6 +663,7 @@ func (r *RunCMD) Run(ctx *cliContext.Context) (result error) {
return err
}
if addonCfg.Enabled {
addonCfg.OfficialLibrary = r.AddonsOfficialLibrary
opts = append(opts, config.WithAddons(addonCfg))
}
if authEnabled {
+18 -4
View File
@@ -19,14 +19,28 @@ const localAddonOwnerID = "local"
// the run flags. Only the on/off switch and the ownership mode are flags; the
// rest follows from the node being its own owner.
//
// The default mode, named-user, needs the account database. Without
// authentication there is no such database, so the owner stays off with a
// warning instead of stopping the server: local-owner mode is never inferred
// and an operator must select it.
// An empty mode takes the documented default: local-owner when authentication
// is off, named-user when it is on. An explicit mode always wins. Named-user
// needs the account database, so an explicit named-user without
// authentication leaves the owner off with a warning instead of stopping the
// server.
func addonConfigFor(enabled bool, mode, dataPath string, authEnabled bool) (config.AddonConfig, error) {
if !enabled {
return config.AddonConfig{}, nil
}
if mode == "" {
// The documented default for an install that did not choose a mode.
// With authentication off there are no accounts, so the one shared
// local owner is what makes add-ons usable; with authentication on,
// every account keeps its own access. This is the one place that
// resolves the default. It is an explicit, documented default for
// auth-off installs, not an inference from the request or the network,
// and a flag or environment value always wins over it.
mode = auth.AddonModeNamedUser
if !authEnabled {
mode = auth.AddonModeLocalOwner
}
}
switch mode {
case auth.AddonModeNamedUser, auth.AddonModeLocalOwner:
default:
+30 -3
View File
@@ -32,7 +32,7 @@ var _ = Describe("Add-on owner flags", Serial, func() {
return &command.Run, err
}
BeforeEach(func() {
for _, key := range []string{"LOCALAI_ADDONS_ENABLED", "LOCALAI_ADDONS_OWNERSHIP_MODE"} {
for _, key := range []string{"LOCALAI_ADDONS_ENABLED", "LOCALAI_ADDONS_OWNERSHIP_MODE", "LOCALAI_ADDONS_OFFICIAL_LIBRARY"} {
value, present := os.LookupEnv(key)
DeferCleanup(func() {
if present {
@@ -45,11 +45,12 @@ var _ = Describe("Add-on owner flags", Serial, func() {
}
})
It("turns the owner on by default, in named-user mode", func() {
It("turns the owner on by default, in named-user mode when authentication is on", func() {
r, err := parse()
Expect(err).NotTo(HaveOccurred())
Expect(r.AddonsEnabled).To(BeTrue())
Expect(r.AddonsOwnershipMode).To(Equal("named-user"))
Expect(r.AddonsOwnershipMode).To(BeEmpty())
Expect(r.AddonsOfficialLibrary).To(BeTrue())
dir := GinkgoT().TempDir()
c, err := addonConfigFor(r.AddonsEnabled, r.AddonsOwnershipMode, dir, true)
Expect(err).NotTo(HaveOccurred())
@@ -65,6 +66,32 @@ var _ = Describe("Add-on owner flags", Serial, func() {
Expect(c.Transport.OwnerAddress).To(Equal(c.OwnerAddress))
})
It("defaults to local-owner when authentication is off", func() {
r, err := parse()
Expect(err).NotTo(HaveOccurred())
c, err := addonConfigFor(r.AddonsEnabled, r.AddonsOwnershipMode, GinkgoT().TempDir(), false)
Expect(err).NotTo(HaveOccurred())
Expect(c.Enabled).To(BeTrue())
Expect(c.OwnershipMode).To(Equal("local-owner"))
})
It("lets an explicit named-user win over the auth-off default", func() {
r, err := parse("--addons-ownership-mode=named-user")
Expect(err).NotTo(HaveOccurred())
c, err := addonConfigFor(r.AddonsEnabled, r.AddonsOwnershipMode, GinkgoT().TempDir(), false)
Expect(err).NotTo(HaveOccurred())
Expect(c).To(Equal(config.AddonConfig{}))
})
It("reads the ownership mode and the official library from the environment", func() {
Expect(os.Setenv("LOCALAI_ADDONS_OWNERSHIP_MODE", "named-user")).To(Succeed())
Expect(os.Setenv("LOCALAI_ADDONS_OFFICIAL_LIBRARY", "false")).To(Succeed())
r, err := parse()
Expect(err).NotTo(HaveOccurred())
Expect(r.AddonsOwnershipMode).To(Equal("named-user"))
Expect(r.AddonsOfficialLibrary).To(BeFalse())
})
It("turns the owner off completely with --addons-enabled=false", func() {
r, err := parse("--addons-enabled=false")
Expect(err).NotTo(HaveOccurred())
+10 -4
View File
@@ -26,12 +26,18 @@ type AddonConfig struct {
InstanceID, OwnerID, OwnerAddress, StorageID, Root string
SharedStorage bool
// OwnershipMode is "named-user" (the default when empty) or "local-owner".
// Local-owner mode is explicit opt-in and never inferred: every caller that
// reaches the addon routes shares one authority and sharing is disabled.
// Local-owner mode is never inferred from a request. The CLI sets it as the
// documented default when authentication is off and the operator chose no
// mode. Every caller that reaches the addon routes shares one authority and
// sharing is disabled.
// Every frontend of a deployment must use the same value.
OwnershipMode string
Host contract.HostCompatibility
AdminCheck func(context.Context, contract.Actor) error `json:"-" yaml:"-"`
// OfficialLibrary registers the official add-on library on the first start,
// when the owner has no sources.json yet. A sources.json that exists is
// never changed by it, so a library the admin removed stays removed.
OfficialLibrary bool
Host contract.HostCompatibility
AdminCheck func(context.Context, contract.Actor) error `json:"-" yaml:"-"`
// BackupKeep is how many backups of one add-on the owner keeps; zero is
// three. BackupMaxAge removes older ones; zero keeps them by count only.
// The newest verified backup and the recovery copy of an upgrade are
+2 -1
View File
@@ -60,7 +60,8 @@ type Option func(*settings)
type settings struct{ mode string }
// WithOwnership selects the ownership mode the deployment configured. The
// default is named-user; local-owner is never inferred from a request.
// default is named-user; local-owner is never inferred from a request. The
// CLI selects it as the documented default of an install with authentication off.
func WithOwnership(mode string) Option {
return func(s *settings) { s.mode = mode }
}