mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-28 09:08:54 -04:00
fix(agents): warn about unowned data with auth (#12669)
Warn before the standalone pool starts root agents with user auth enabled. Count root agents, collections, skills, and configured skill repositories. Document the auth transition, bootstrap rules, and agent key behavior. Addresses the warning and documentation step of #12665. Admin visibility and data migration remain separate work. Assisted-by: Codex:GPT-6 Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
This commit is contained in:
1 parent
e28d5009d9
commit
f02d5244da
4 files changed
+201
No files matched your search
@@ -358,6 +358,9 @@ func (s *AgentPoolService) startLocalAGI(_ context.Context, cfg config.AgentPool
|
||||
agiServices.FiltersConfigMeta(),
|
||||
)
|
||||
|
||||
// Warn before starting legacy agents, which remain outside authenticated users' storage.
|
||||
s.warnLegacyAuthData()
|
||||
|
||||
// Start all agents
|
||||
if err := pool.StartAll(); err != nil {
|
||||
xlog.Error("Failed to start agent pool", "error", err)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
package agentpool
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
skillsManager "github.com/mudler/LocalAI/core/services/skills"
|
||||
skillgit "github.com/mudler/skillserver/pkg/git"
|
||||
"github.com/mudler/xlog"
|
||||
)
|
||||
|
||||
func (s *AgentPoolService) warnLegacyAuthData() {
|
||||
if !s.appConfig.Auth.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
rootAgents := 0
|
||||
for _, name := range s.localAGI.pool.List() {
|
||||
if !strings.Contains(name, ":") {
|
||||
rootAgents++
|
||||
}
|
||||
}
|
||||
rootCollections, err := s.collectionsBackend.ListCollections()
|
||||
if err != nil {
|
||||
xlog.Warn("Could not check root collections after enabling user authentication", "error", err)
|
||||
}
|
||||
|
||||
skillCount, repoCount := 0, 0
|
||||
if s.localAGI.skillsService != nil {
|
||||
manager := skillsManager.NewFilesystemManager(s.localAGI.skillsService)
|
||||
rootSkills, err := manager.List()
|
||||
if err != nil {
|
||||
xlog.Warn("Could not check root skills after enabling user authentication", "error", err)
|
||||
} else {
|
||||
skillCount = len(rootSkills)
|
||||
}
|
||||
// Count disabled and not-yet-cloned repositories too; the skill index
|
||||
// contains only skills that are already available on disk.
|
||||
repos, err := skillgit.NewConfigManager(s.localAGI.skillsService.GetSkillsDir()).LoadConfig()
|
||||
if err != nil {
|
||||
xlog.Warn("Could not check root skill repositories after enabling user authentication", "error", err)
|
||||
} else {
|
||||
repoCount = len(repos)
|
||||
}
|
||||
}
|
||||
|
||||
if rootAgents+len(rootCollections)+skillCount+repoCount == 0 {
|
||||
return
|
||||
}
|
||||
xlog.Warn("User authentication is enabled but root agent data has no owner",
|
||||
"agents", rootAgents, "collections", len(rootCollections), "skills", skillCount, "git_repositories", repoCount,
|
||||
"stateDir", s.stateDir,
|
||||
"details", "Existing data is not migrated to a user. Root agents still start and may run scheduled tasks; their API calls require valid credentials.",
|
||||
"docs", "https://localai.io/features/authentication/#enabling-auth-on-an-existing-instance")
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
package agentpool_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/LocalAI/core/config"
|
||||
"github.com/mudler/LocalAI/core/services/agentpool"
|
||||
"github.com/mudler/xlog"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("legacy auth warning", func() {
|
||||
const warning = "User authentication is enabled but root agent data has no owner"
|
||||
var dir string
|
||||
var logFile *os.File
|
||||
|
||||
BeforeEach(func() {
|
||||
dir = GinkgoT().TempDir()
|
||||
var err error
|
||||
logFile, err = os.CreateTemp(dir, "startup-*.log")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
xlog.SetLogger(xlog.NewLoggerWithHandler(slog.NewTextHandler(logFile, nil), xlog.LogLevelInfo))
|
||||
})
|
||||
AfterEach(func() {
|
||||
xlog.SetLogger(xlog.NewLogger(xlog.LogLevelInfo, "text"))
|
||||
Expect(logFile.Close()).To(Succeed())
|
||||
})
|
||||
|
||||
write := func(path, data string) {
|
||||
Expect(os.MkdirAll(filepath.Dir(path), 0750)).To(Succeed())
|
||||
Expect(os.WriteFile(path, []byte(data), 0600)).To(Succeed())
|
||||
}
|
||||
start := func(enabled bool, collectionPath string) string {
|
||||
cfg := config.NewApplicationConfig()
|
||||
cfg.Auth.Enabled = enabled
|
||||
cfg.AgentPool = config.AgentPoolConfig{
|
||||
Enabled: true, StateDir: dir, APIURL: "http://127.0.0.1:1",
|
||||
DefaultModel: "unused", CollectionDBPath: collectionPath,
|
||||
}
|
||||
svc, err := agentpool.NewAgentPoolService(cfg)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(svc.Start(context.Background())).To(Succeed())
|
||||
svc.Stop()
|
||||
data, err := os.ReadFile(logFile.Name())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return string(data)
|
||||
}
|
||||
|
||||
It("warns about root agents, custom-path collections, skills and git repositories", func() {
|
||||
write(filepath.Join(dir, "pool.json"), `{"legacy":{"name":"legacy","model":"unused"},"owner:private":{"name":"owner:private","model":"unused"}}`)
|
||||
collectionsDir := filepath.Join(GinkgoT().TempDir(), "collections")
|
||||
write(filepath.Join(collectionsDir, "collection-notes.json"), `{}`)
|
||||
write(filepath.Join(dir, "skills", "legacy-skill", "SKILL.md"), "---\nname: legacy-skill\ndescription: A legacy skill\n---\nInstructions\n")
|
||||
write(filepath.Join(dir, "skills", ".git-repos.json"), `[{"id":"old","name":"old","url":"https://example.invalid/old.git","enabled":false}]`)
|
||||
logs := start(true, collectionsDir)
|
||||
Expect(logs).To(ContainSubstring(warning))
|
||||
Expect(logs).To(ContainSubstring("agents=1 collections=1 skills=1 git_repositories=1"))
|
||||
Expect(logs).To(ContainSubstring("authentication/#enabling-auth-on-an-existing-instance"))
|
||||
Expect(logs).NotTo(ContainSubstring("agents=2"))
|
||||
Expect(strings.Index(logs, warning)).To(BeNumerically("<", strings.Index(logs, "Creating agent")))
|
||||
})
|
||||
|
||||
It("does not warn when auth is disabled", func() {
|
||||
write(filepath.Join(dir, "skills", "legacy-skill", "SKILL.md"), "---\nname: legacy-skill\ndescription: A legacy skill\n---\nInstructions\n")
|
||||
Expect(start(false, "")).NotTo(ContainSubstring(warning))
|
||||
})
|
||||
|
||||
It("does not count user-owned data as root data", func() {
|
||||
write(filepath.Join(dir, "pool.json"), `{"owner:private":{"name":"owner:private","model":"unused"}}`)
|
||||
write(filepath.Join(dir, "users", "owner", "collections", "collection-notes.json"), `{}`)
|
||||
write(filepath.Join(dir, "users", "owner", "skills", "private", "SKILL.md"), "---\nname: private\ndescription: Private skill\n---\nInstructions\n")
|
||||
Expect(start(true, "")).NotTo(ContainSubstring(warning))
|
||||
})
|
||||
|
||||
It("warns for configured git repositories even without installed skills", func() {
|
||||
write(filepath.Join(dir, "skills", ".git-repos.json"), `[{"id":"old","name":"old","url":"https://example.invalid/old.git","enabled":false}]`)
|
||||
logs := start(true, "")
|
||||
Expect(logs).To(ContainSubstring(warning))
|
||||
Expect(logs).To(ContainSubstring("agents=0 collections=0 skills=0 git_repositories=1"))
|
||||
})
|
||||
|
||||
It("reports an unreadable repository config without preventing startup", func() {
|
||||
write(filepath.Join(dir, "collections", "collection-notes.json"), `{}`)
|
||||
write(filepath.Join(dir, "skills", ".git-repos.json"), `invalid json`)
|
||||
logs := start(true, "")
|
||||
Expect(logs).To(ContainSubstring("Could not check root skill repositories"))
|
||||
Expect(logs).To(ContainSubstring(warning))
|
||||
Expect(logs).To(ContainSubstring("agents=0 collections=1"))
|
||||
})
|
||||
})
|
||||
@@ -118,6 +118,53 @@ LOCALAI_AUTH_DATABASE_URL=postgres://user:pass@host/dbname \
|
||||
localai run
|
||||
```
|
||||
|
||||
### Enabling auth on an existing instance
|
||||
|
||||
Enabling user authentication does not assign existing agent data to an account.
|
||||
Data created without user auth belongs to the root storage area (an empty user ID).
|
||||
Authenticated users get separate storage, including admins. Existing agents,
|
||||
collections, skills, tasks, jobs, and outputs can therefore disappear from their
|
||||
API and Web UI views while remaining on disk.
|
||||
|
||||
In standalone (LocalAGI) mode, startup logs a warning when it finds root agents,
|
||||
collections, skills, or configured skill repositories. The warning includes counts
|
||||
and a link to this section. It checks the configured root collection path and
|
||||
excludes user-owned data. It does not inventory tasks, jobs, outputs, or agent state.
|
||||
An absent warning is not proof that every resource has an owner.
|
||||
|
||||
{{% notice warning %}}
|
||||
The warning does not migrate, delete, or pause anything. Root agents still start,
|
||||
including their scheduled runs. Their requests to the local API need valid
|
||||
credentials after auth is enabled. Admin views do not provide a complete migration
|
||||
or export path for root data.
|
||||
{{% /notice %}}
|
||||
|
||||
Before switching an existing instance:
|
||||
|
||||
1. Pause existing agents and scheduled work while auth is still off.
|
||||
2. Stop LocalAI and back up its data, agent state, and any separately configured collection storage or databases.
|
||||
3. Test the switch on a copy before changing the production instance.
|
||||
4. Restrict access during bootstrap and register the intended administrator first.
|
||||
5. Recreate or import agent configurations under the intended user. Recreate collections and skills there as needed.
|
||||
6. Verify credentials and required data before resuming agents.
|
||||
|
||||
The first registered user becomes admin and bypasses approval or invite requirements.
|
||||
Setting `LOCALAI_ADMIN_EMAIL` promotes that email address, but does **not** reserve
|
||||
the first-admin role for it. Keep registration inaccessible to other users until
|
||||
bootstrap is complete.
|
||||
|
||||
Recreating an agent configuration does not migrate its character, scheduler state,
|
||||
or long-term memory. Keep the backup: LocalAI has no automatic migration of the
|
||||
root storage area, and agent configuration export does not include this state.
|
||||
|
||||
With user auth active, creating, importing, or updating an agent without
|
||||
`api_key` generates a new key owned by that user. The key inherits the owner's
|
||||
permissions; an admin-owned key can therefore grant admin access. Repeated updates
|
||||
that omit `api_key` can create additional keys. Supply an explicit key with the
|
||||
intended permissions when recreating or updating agents, and review existing keys
|
||||
in **Settings → Users and keys**. Existing root agents can use an explicit
|
||||
`api_key` or `LOCALAI_AGENT_POOL_API_KEY`; neither setting transfers ownership.
|
||||
|
||||
### Configuration Reference
|
||||
|
||||
| Environment Variable | Default | Description |
|
||||
|
||||
Reference in new issue
Block a user