Skip to content

Commit 1a1fce4

Browse files
committed
refactoring and consolidation
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent eed4052 commit 1a1fce4

42 files changed

Lines changed: 270 additions & 269 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

core/application/distributed.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/mudler/LocalAI/core/services/messaging"
1414
"github.com/mudler/LocalAI/core/services/nodes"
1515
"github.com/mudler/LocalAI/core/services/storage"
16+
"github.com/mudler/LocalAI/pkg/sanitize"
1617
"github.com/mudler/xlog"
1718
"gorm.io/gorm"
1819
)
@@ -50,7 +51,7 @@ func initDistributed(cfg *config.ApplicationConfig, authDB *gorm.DB) (*Distribut
5051
return nil, fmt.Errorf("distributed mode requires authentication to be enabled (--auth / LOCALAI_AUTH=true)")
5152
}
5253
if !isPostgresURL(cfg.Auth.DatabaseURL) {
53-
return nil, fmt.Errorf("distributed mode requires PostgreSQL for auth database (got %q)", cfg.Auth.DatabaseURL)
54+
return nil, fmt.Errorf("distributed mode requires PostgreSQL for auth database (got %q)", sanitize.URL(cfg.Auth.DatabaseURL))
5455
}
5556

5657
// Validate NATS
@@ -69,7 +70,7 @@ func initDistributed(cfg *config.ApplicationConfig, authDB *gorm.DB) (*Distribut
6970
if err != nil {
7071
return nil, fmt.Errorf("connecting to NATS: %w", err)
7172
}
72-
xlog.Info("Connected to NATS", "url", cfg.Distributed.NatsURL)
73+
xlog.Info("Connected to NATS", "url", sanitize.URL(cfg.Distributed.NatsURL))
7374

7475
// Ensure NATS is closed if any subsequent initialization step fails.
7576
success := false
@@ -143,7 +144,7 @@ func initDistributed(cfg *config.ApplicationConfig, authDB *gorm.DB) (*Distribut
143144
xlog.Info("Distributed job store initialized")
144145

145146
// Initialize job dispatcher
146-
dispatcher := jobs.NewDispatcher(jobStore, natsClient, authDB, cfg.Distributed.InstanceID)
147+
dispatcher := jobs.NewDispatcher(jobStore, natsClient, authDB, cfg.Distributed.InstanceID, cfg.Distributed.JobWorkerConcurrency)
147148

148149
// Initialize agent store
149150
agentStore, err := agents.NewAgentStore(authDB)

core/application/startup.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/mudler/LocalAI/internal"
2222

2323
"github.com/mudler/LocalAI/pkg/model"
24+
"github.com/mudler/LocalAI/pkg/sanitize"
2425
"github.com/mudler/LocalAI/pkg/xsysinfo"
2526
"github.com/mudler/xlog"
2627
)
@@ -104,7 +105,7 @@ func New(opts ...config.AppOption) (*Application, error) {
104105
return nil, fmt.Errorf("failed to initialize auth database: %w", err)
105106
}
106107
application.authDB = authDB
107-
xlog.Info("Auth enabled", "database", options.Auth.DatabaseURL)
108+
xlog.Info("Auth enabled", "database", sanitize.URL(options.Auth.DatabaseURL))
108109

109110
// Start session and expired API key cleanup goroutine
110111
go func() {

core/cli/agent_worker.go

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/mudler/LocalAI/core/services/jobs"
2020
mcpRemote "github.com/mudler/LocalAI/core/services/mcp"
2121
"github.com/mudler/LocalAI/core/services/messaging"
22+
"github.com/mudler/LocalAI/pkg/sanitize"
2223
"github.com/mudler/cogito"
2324
"github.com/mudler/cogito/clients"
2425
"github.com/mudler/xlog"
@@ -53,7 +54,7 @@ type AgentWorkerCMD struct {
5354
}
5455

5556
func (cmd *AgentWorkerCMD) Run(ctx *cliContext.Context) error {
56-
xlog.Info("Starting agent worker", "nats", cmd.NatsURL, "register_to", cmd.RegisterTo)
57+
xlog.Info("Starting agent worker", "nats", sanitize.URL(cmd.NatsURL), "register_to", cmd.RegisterTo)
5758

5859
// Resolve API URL
5960
apiURL := cmp.Or(cmd.APIURL, strings.TrimRight(cmd.RegisterTo, "/"))
@@ -118,11 +119,12 @@ func (cmd *AgentWorkerCMD) Run(ctx *cliContext.Context) error {
118119
// Create and start the NATS dispatcher.
119120
// No ConfigProvider or SkillStore needed — config and skills arrive in the job payload.
120121
dispatcher := agents.NewNATSDispatcher(
121-
&natsClientAdapter{natsClient},
122+
natsClient,
122123
eventBridge,
123124
nil, // no ConfigProvider: config comes in the enriched NATS payload
124125
apiURL, cmd.APIToken,
125126
cmd.Subject, cmd.Queue,
127+
0, // no concurrency limit (CLI worker)
126128
)
127129

128130
if err := dispatcher.Start(shutdownCtx); err != nil {
@@ -273,21 +275,6 @@ func sendMCPDiscoveryReply(reply func([]byte), servers []mcpRemote.MCPServerInfo
273275
reply(data)
274276
}
275277

276-
// natsClientAdapter wraps messaging.Client to satisfy agents.NATSClient.
277-
// The concrete Client.QueueSubscribe returns *nats.Subscription; this adapter
278-
// widens it to messaging.Subscription so it matches the interface.
279-
type natsClientAdapter struct {
280-
client *messaging.Client
281-
}
282-
283-
func (a *natsClientAdapter) Publish(subject string, data any) error {
284-
return a.client.Publish(subject, data)
285-
}
286-
287-
func (a *natsClientAdapter) QueueSubscribe(subject, queue string, handler func(data []byte)) (messaging.Subscription, error) {
288-
return a.client.QueueSubscribe(subject, queue, handler)
289-
}
290-
291278
// handleMCPCIJob processes an MCP CI job on the agent worker.
292279
// The agent worker can create MCP sessions (has docker) and call the LocalAI API for inference.
293280
func handleMCPCIJob(shutdownCtx context.Context, data []byte, apiURL, apiToken string, natsClient *messaging.Client) {

core/cli/worker.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/mudler/LocalAI/core/services/messaging"
2525
"github.com/mudler/LocalAI/core/services/nodes"
2626
"github.com/mudler/LocalAI/core/services/storage"
27+
"github.com/mudler/LocalAI/pkg/sanitize"
2728
grpc "github.com/mudler/LocalAI/pkg/grpc"
2829
"github.com/mudler/LocalAI/pkg/model"
2930
"github.com/mudler/LocalAI/pkg/system"
@@ -126,7 +127,7 @@ func (cmd *WorkerCMD) Run(ctx *cliContext.Context) error {
126127
}
127128

128129
// Connect to NATS
129-
xlog.Info("Connecting to NATS", "url", cmd.NatsURL)
130+
xlog.Info("Connecting to NATS", "url", sanitize.URL(cmd.NatsURL))
130131
natsClient, err := messaging.New(cmd.NatsURL)
131132
if err != nil {
132133
return fmt.Errorf("connecting to NATS: %w", err)

core/config/distributed_config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ type DistributedConfig struct {
3030
StaleNodeThreshold time.Duration // Time before a node is considered stale (default 60s)
3131

3232
MaxUploadSize int64 // Maximum upload body size in bytes (default 50 GB)
33+
34+
AgentWorkerConcurrency int `yaml:"agent_worker_concurrency" json:"agent_worker_concurrency" env:"LOCALAI_AGENT_WORKER_CONCURRENCY"`
35+
JobWorkerConcurrency int `yaml:"job_worker_concurrency" json:"job_worker_concurrency" env:"LOCALAI_JOB_WORKER_CONCURRENCY"`
3336
}
3437

3538
// Distributed config options

core/config/model_config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ type ModelConfig struct {
5050
InputToken [][]int `yaml:"-" json:"-"`
5151
functionCallString, functionCallNameString string `yaml:"-" json:"-"`
5252
ResponseFormat string `yaml:"-" json:"-"`
53-
ResponseFormatMap map[string]interface{} `yaml:"-" json:"-"`
53+
ResponseFormatMap map[string]any `yaml:"-" json:"-"`
5454

5555
FunctionsConfig functions.FunctionsConfig `yaml:"function,omitempty" json:"function,omitempty"`
5656
ReasoningConfig reasoning.Config `yaml:"reasoning,omitempty" json:"reasoning,omitempty"`

core/http/endpoints/localai/agents.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import (
44
"encoding/json"
55
"fmt"
66
"io"
7+
"maps"
78
"net/http"
89
"os"
910
"path/filepath"
10-
"sort"
11+
"slices"
1112
"strings"
1213

1314
"github.com/labstack/echo/v4"
@@ -76,11 +77,7 @@ func ListAgentsEndpoint(app *application.Application) echo.HandlerFunc {
7677
svc := app.AgentPoolService()
7778
userID := getUserID(c)
7879
statuses := svc.ListAgentsForUser(userID)
79-
agents := make([]string, 0, len(statuses))
80-
for name := range statuses {
81-
agents = append(agents, name)
82-
}
83-
sort.Strings(agents)
80+
agents := slices.Sorted(maps.Keys(statuses))
8481
resp := map[string]any{
8582
"agents": agents,
8683
"agentCount": len(agents),

core/http/endpoints/localai/nodes.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,9 @@ func provisionAgentWorkerKey(authDB *gorm.DB, registry *nodes.NodeRegistry, node
211211

212212
node.AuthUserID = workerUser.ID
213213
node.APIKeyID = apiKey.ID
214-
registry.UpdateAuthRefs(node.ID, workerUser.ID, apiKey.ID)
214+
if err := registry.UpdateAuthRefs(node.ID, workerUser.ID, apiKey.ID); err != nil {
215+
xlog.Warn("Failed to update auth refs on node", "node", node.Name, "error", err)
216+
}
215217

216218
// Grant collections feature so the worker can store/retrieve KB data on behalf of users.
217219
perm := &auth.UserPermission{

core/http/routes/nodes.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ func nodeReadyMiddleware(registry *nodes.NodeRegistry) echo.MiddlewareFunc {
2929
// nodes themselves (register, heartbeat, drain, query own models, deregister).
3030
// These are authenticated via the registration token, not admin middleware.
3131
//
32-
// TODO(security): Self-service routes currently accept any node :id with a valid
33-
// registration token. A compromised worker can heartbeat/drain/deregister other nodes.
34-
// To fix: issue per-node bearer tokens during registration and validate :id matches
35-
// the authenticated node. See security review item S2.
32+
// TODO(security): Node self-service endpoints authenticate via shared registration
33+
// token but do not verify per-node identity. A compromised worker can heartbeat/drain/
34+
// deregister other nodes. Future: issue per-node JWT at registration, validate node
35+
// identity on subsequent requests (compare :id param with token subject).
3636
func RegisterNodeSelfServiceRoutes(e *echo.Echo, registry *nodes.NodeRegistry, registrationToken string, autoApprove bool, authDB *gorm.DB, hmacSecret string) {
3737
if registry == nil {
3838
return
@@ -84,9 +84,14 @@ func RegisterNodeAdminRoutes(e *echo.Echo, registry *nodes.NodeRegistry, unloade
8484
e.GET("/ws/nodes/:id/backend-logs/:modelId", localai.NodeBackendLogsWSEndpoint(registry, registrationToken), readyMw, adminMw)
8585
}
8686

87-
// nodeTokenAuth returns middleware that validates the registration token
88-
// from an Authorization: Bearer <token> header using constant-time comparison.
89-
// If registrationToken is empty, all requests are allowed (no auth required).
87+
// nodeTokenAuth validates the registration token for node self-service endpoints.
88+
// When registrationToken is empty (single-node / non-distributed mode), these
89+
// endpoints are unprotected. This is intentional: in single-node mode there are
90+
// no remote workers to authenticate. Operators enabling distributed mode MUST
91+
// set a registration token via LOCALAI_REGISTRATION_TOKEN or config.
92+
//
93+
// It validates the token from an Authorization: Bearer <token> header using
94+
// constant-time comparison.
9095
func nodeTokenAuth(registrationToken string) echo.MiddlewareFunc {
9196
return func(next echo.HandlerFunc) echo.HandlerFunc {
9297
return func(c echo.Context) error {

core/http/routes/ui_api.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import (
99
"net/http"
1010
"net/url"
1111
"path"
12-
"sort"
12+
"cmp"
13+
"slices"
1314
"strconv"
1415
"strings"
1516
"sync"
@@ -153,17 +154,17 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model
153154
}
154155

155156
// Sort operations by progress (ascending), then by ID for stable display order
156-
sort.Slice(operations, func(i, j int) bool {
157-
progressI := operations[i]["progress"].(int)
158-
progressJ := operations[j]["progress"].(int)
157+
slices.SortFunc(operations, func(a, b map[string]interface{}) int {
158+
progressA := a["progress"].(int)
159+
progressB := b["progress"].(int)
159160

160161
// Primary sort by progress
161-
if progressI != progressJ {
162-
return progressI < progressJ
162+
if progressA != progressB {
163+
return cmp.Compare(progressA, progressB)
163164
}
164165

165166
// Secondary sort by ID for stability when progress is the same
166-
return operations[i]["id"].(string) < operations[j]["id"].(string)
167+
return cmp.Compare(a["id"].(string), b["id"].(string))
167168
})
168169

169170
return c.JSON(200, map[string]interface{}{
@@ -239,7 +240,7 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model
239240
for t := range allTags {
240241
tags = append(tags, t)
241242
}
242-
sort.Strings(tags)
243+
slices.Sort(tags)
243244

244245
// Get all available backends (before filtering so dropdown always shows all)
245246
allBackendsMap := map[string]struct{}{}
@@ -252,7 +253,7 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model
252253
for b := range allBackendsMap {
253254
backendNames = append(backendNames, b)
254255
}
255-
sort.Strings(backendNames)
256+
slices.Sort(backendNames)
256257

257258
if tag != "" {
258259
models = gallery.GalleryElements[*gallery.GalleryModel](models).FilterByTag(tag)
@@ -809,7 +810,7 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model
809810
for t := range allTags {
810811
tags = append(tags, t)
811812
}
812-
sort.Strings(tags)
813+
slices.Sort(tags)
813814

814815
if tag != "" {
815816
backends = gallery.GalleryElements[*gallery.GalleryBackend](backends).FilterByTag(tag)

0 commit comments

Comments
 (0)