mirror of
https://github.com/mudler/LocalAI.git
synced 2026-09-22 22:24:54 -04:00
The three NATS queue groups jobs.new, jobs.mcp-ci.new and agent.execute are gone. Dispatching work is now a row in a work_claims table, taken by one frontend replica with SELECT ... FOR UPDATE SKIP LOCKED and driven on an agent worker as a streaming control RPC over that worker's tunnel. Exactly-one delivery among competing consumers is a database problem, not a broker feature. An agent worker has no database, so it never claims; it executes what the claiming replica hands it. A claim must not outlive the replica that took it. The reap releases a claim whose owner is no longer a live replica in the instances table, on the database clock, and never asks how long the claim has been held. A job that legitimately runs for an hour on a heartbeating replica is left alone, while a claim whose owner stopped heartbeating becomes claimable again within one liveness window. A replica with no advertised address has no instances row at all, so it refuses to claim rather than have its work reaped out from under it mid-run. The settle rule is stated once, in settleClaim, and every exit path calls it. A transport failure releases the claim and never completes or discards it; only a decoded reply line completes it. That line is deliberately not cluster.IsWorkerAnswer, which accepts the stream refusals a worker's tunnel writes before any request body reaches its control server: completing on those would discard work that never ran. The terminal line is persisted before the claim is completed, so a store that refuses leaves the claim standing rather than leaving the job running for ever. That is the dropped-result defect fixed structurally rather than by retry. This also surfaces a pre-existing gap rather than causing one: no worker has ever served plain task jobs, and publishing them into an empty queue group left them running with no trace. Such a claim is now failed with a reason. Removes QueueWorkers, --agent-subject and --agent-queue, and narrows an agent worker's minted JWT by agent.execute and jobs.mcp-ci.new. Assisted-by: Claude Opus 5 [claude-code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
73 lines
2.4 KiB
Go
73 lines
2.4 KiB
Go
package natsauth_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mudler/LocalAI/pkg/natsauth"
|
|
"github.com/nats-io/jwt/v2"
|
|
"github.com/nats-io/nkeys"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
func TestNatsAuth(t *testing.T) {
|
|
RegisterFailHandler(Fail)
|
|
RunSpecs(t, "NatsAuth")
|
|
}
|
|
|
|
var _ = Describe("MintWorkerJWT", func() {
|
|
var accountSeed string
|
|
|
|
BeforeEach(func() {
|
|
akp, err := nkeys.CreateAccount()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
seed, err := akp.Seed()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
accountSeed = string(seed)
|
|
})
|
|
|
|
It("mints a JWT with backend worker permissions", func() {
|
|
cfg := natsauth.Config{AccountSeed: accountSeed, WorkerJWTTTL: time.Hour}
|
|
token, seed, err := cfg.MintWorkerJWT("550e8400-e29b-41d4-a716-446655440000", "backend")
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(token).NotTo(BeEmpty())
|
|
Expect(seed).NotTo(BeEmpty())
|
|
|
|
uc, err := jwt.DecodeUserClaims(token)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
// A backend worker opens no bus connection at all, so its node subtree
|
|
// went with it. The JWT is still minted at registration and simply
|
|
// unused; asserting BOTH lists are exactly the inbox is what keeps it
|
|
// from silently becoming an unrestricted credential, since NATS reads
|
|
// an empty allow list as no restriction.
|
|
Expect(uc.Permissions.Sub.Allow).To(ConsistOf("_INBOX.>"))
|
|
// The install-progress subject is gone with the carrier: progress is a
|
|
// line in the install response now, so a minted worker JWT must not
|
|
// still be granted a publish right for it. File staging went the same
|
|
// way, so a minted worker JWT publishes nowhere but its own inbox.
|
|
Expect(uc.Permissions.Pub.Allow).To(ConsistOf("_INBOX.>"))
|
|
for _, subj := range uc.Permissions.Pub.Allow {
|
|
Expect(subj).NotTo(ContainSubstring("backend.install"))
|
|
}
|
|
})
|
|
|
|
It("mints agent permissions without backend install subscribe", func() {
|
|
cfg := natsauth.Config{AccountSeed: accountSeed}
|
|
token, _, err := cfg.MintWorkerJWT("node-1", "agent")
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
uc, err := jwt.DecodeUserClaims(token)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(uc.Permissions.Sub.Allow).To(ContainElement("agent.*.cancel"))
|
|
for _, subj := range uc.Permissions.Sub.Allow {
|
|
Expect(subj).NotTo(ContainSubstring("backend.install"))
|
|
}
|
|
})
|
|
|
|
It("rejects mint without account seed", func() {
|
|
_, _, err := (natsauth.Config{}).MintWorkerJWT("id", "backend")
|
|
Expect(err).To(HaveOccurred())
|
|
})
|
|
})
|