Skip to content

feat(distributed): run distributed mode without a message broker - #11812

Open
localai-org-maint-bot wants to merge 158 commits into
masterfrom
test/distributed-e2e-ci
Open

localai-org-maint-bot wants to merge 158 commits into
masterfrom
test/distributed-e2e-ci

Conversation

@localai-org-maint-bot

@localai-org-maint-bot localai-org-maint-bot commented Aug 31, 2026 •

Copy link
Copy Markdown
Collaborator

Distributed mode without a message broker

Running LocalAI distributed used to mean running and managing a NATS cluster, and giving every worker a routable address with inbound ports open back to it. Both are gone.

A worker now dials the frontend load balancer over ordinary HTTP and holds one multiplexed yamux tunnel that carries gRPC, HTTP and websocket traffic together. That tunnel lands on exactly one frontend replica; every other replica reaches the worker by relaying through the owner. Workers bind loopback only and advertise nothing. A deployment needs PostgreSQL and the frontends' own HTTP listener, and nothing else.

github.com/nats-io/... is out of go.mod and go.sum.

What a worker needs now

One outbound HTTPS connection to the same load balancer a browser would use. No routable address, no inbound ports, no broker credentials.

How it works

  • The worker dials GET /api/cluster/connect and holds a yamux session. Control RPCs, file staging and inference all ride it.
  • Tunnel ownership is fenced by a PostgreSQL sequence-backed epoch, and ownership resolution joins live replicas so a dead one can never be named as owner.
  • A replica that does not hold a worker relays to the one that does.
  • Workers authenticate with a per-node credential minted at registration and stored only as a hash.
  • Fan-out runs on PostgreSQL LISTEN/NOTIFY. Payloads above the 8000-byte notification cap become a row plus a notify-with-id, never a truncated payload.
  • Competing consumers use a SELECT ... FOR UPDATE SKIP LOCKED claim queue. A claim is released when the replica holding it stops being live, with no age term, so a slow claimant is never robbed.
  • Absence is read from PostgreSQL rather than inferred from a bus timeout.

The invariant this rests on

Four conditions must never be reported as each other: a routing fact, a connection absent within the reconnect grace, an unreachable peer, and the worker's own answer. Only the first and last may be acted on. Absence makes the scheduler reap rows and evict models, and one of those paths runs during inference, so a collapse between them is not a cosmetic bug.

Review found and fixed fourteen instances of that collapse, including one created by the fix for another, one where an expired caller deadline was blamed on the peer under contention, and one where a worker's own refusal was read as "no route", which left permanently unreapable ghost rows.

Operator impact

  • Upgrade frontends first, then restart workers one at a time. During the window, not-yet-restarted workers show healthy while their models return "no route", and it clears on restart. The reverse order fails: an old frontend rejects a new worker's registration and the worker exits.
  • LOCALAI_ADVERTISE_ADDR and LOCALAI_ADVERTISE_HTTP_ADDR are no longer used.
  • --nats-url is accepted and ignored so existing command lines keep starting. --nats-require-auth no longer gates agent-worker approval.
  • Rolling a frontend back requires restarting every worker, because registration is the only writer of the address columns.

Testing

A real-process end-to-end suite runs a two-frontend, two-worker cluster of live local-ai binaries, which is the configuration where the owner path and the relay path are live at once:

  • Both workers serve inference from both replicas. The relay case sends the first request for a model to the replica that does not own the worker, so the backend install, the model-file staging over the tunnel and the gRPC load and predict all cross the relay. Warming it at its owner first would have left only the predict on the relayed path.
  • Which worker served a request is read back from the database, and the relay is proven by resolving ownership through the production query before the request and re-reading it after. Deleting the relay reddens only the request sent to the non-owner.
  • Killing the replica that owns a worker's tunnel: nothing acts on the absence inside the grace, the worker re-homes to the survivor and serves again, and the other worker keeps serving throughout. Setting the grace to a nanosecond reddens exactly that spec.
  • Cross-replica fan-out is proven between two live replicas sharing one database, including a payload above the 8000-byte notification cap making the round trip byte-identical through the spill row, with the size asserted absolutely so moving the cap in either direction reddens it.
  • A suite-wide negative control blocks both tunnel dials while registration and heartbeats continue: every request must be refused naming the routing fact rather than absence, both workers must stay healthy, and restoring one tunnel must recover exactly that worker.

The suite previously could not fail honestly: with no binary present it ran zero specs, exited zero and printed "Test Suite Passed". It now builds the binary and refuses to run against one older than the tree. That guard has since caught three real mistakes.

The race detector runs clean across 98 suites and 7095 specs with no known-failure allowance.

Bugs found in existing code and fixed

  • Unloading a model reported success when the free failed, so a failed eviction told the scheduler VRAM had been freed.
  • Status readers were handed a live pointer that broadcast subscribers mutate, a real race on the operations endpoint.
  • A progress tick delivered after a cancel cleared the cancelled flag.
  • Two end-to-end specs had been red for an entire phase because a label filter excluded them.

Follow-ups closed before merge

The gaps found during the work were closed rather than deferred:

  • Peer-link identity. /api/cluster/peer took a self-declared replica id, so anything holding the shared registration token could relay to every worker a replica owned and repeatedly evict its inbound link. Each replica now mints its own secret at startup and publishes only the hash; the plaintext never leaves the process that minted it. A mixed-version rollout refuses rather than falling back, because an old replica and an attacker send byte-identical requests.
  • Port allocation now checks a port is actually free instead of trusting bookkeeping, and no longer hands out ports from the kernel's ephemeral range.
  • Two unbounded maps bounded: health-monitor miss streaks, and a departed node's cached HTTP clients.
  • A failed claim backs off instead of respinning.
  • Three pre-existing data races fixed, so the race detector runs with no allowance.
  • Skills and collection caches are documented as replica-local rather than given invalidation, because every read verb hits local files: telling a peer to invalidate would only make it re-derive from a directory that lacks the change. The two consequences are documented.

Known remaining

  • DistributedManager.List truncates skill content to 500 bytes for agents, a separate pre-existing defect.
  • MCP prompts and resources have no distributed path, pre-existing and already broken before this work.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y2TjpXdY7SszRrM5PhSp1e

Comment thread tests/e2e/distributed/cluster/admin.go Fixed
Comment thread tests/e2e/distributed/cluster/cluster.go Fixed
Comment thread tests/e2e/distributed/cluster/cluster.go Fixed
Comment thread tests/e2e/distributed/cluster/cluster.go Fixed
Comment thread tests/e2e/distributed/cluster/cluster.go Fixed
Comment thread tests/e2e/distributed/cluster/cluster.go Fixed
Comment thread tests/e2e/distributed/cluster/cluster.go Fixed
Comment thread tests/e2e/distributed/cluster/cluster.go Fixed
Comment thread tests/e2e/distributed/cluster/cluster.go Fixed
Comment thread tests/e2e/distributed/cluster/cluster.go Fixed
@localai-org-maint-bot localai-org-maint-bot changed the title test(distributed): run distributed mode in CI, and test replica failover feat(distributed): workers no longer need inbound ports (worker tunnel) Sep 2, 2026
Comment thread tests/e2e/distributed/cluster/cluster.go Fixed
Comment thread tests/e2e/distributed/cluster/cluster.go Fixed
Comment thread tests/e2e/distributed/cluster/cluster.go Fixed
Comment thread core/services/worker/tunnel.go Fixed
Comment thread core/services/cluster/tunnelproto.go Fixed
@localai-org-maint-bot localai-org-maint-bot changed the title feat(distributed): workers no longer need inbound ports (worker tunnel) feat(distributed): run distributed mode without a message broker Sep 5, 2026
Comment thread tests/e2e/distributed/cluster/cluster.go Fixed
Comment thread core/services/nodes/agent_selector.go Fixed
@mudler

mudler commented Sep 7, 2026

Copy link
Copy Markdown
Owner

@localai-org-maint-bot fix tests

@localai-org-maint-bot

localai-org-maint-bot commented Sep 7, 2026 •

Copy link
Copy Markdown
Collaborator Author

Pushed two test fixes on top of the latest master:

  • 994ec9dde: put the existing macOS Docker skip in sharedPostgres, so the maintenance-connection test cannot bypass it.
  • 4e3224922: stop the general test-e2e target from recursively collecting distributed/cluster suites. Those retain their dedicated targets, which provision PostgreSQL and build the required host binaries.

Verification: the maintenance test fails before the change and skips afterward with a Go overlay selecting the macOS path; four FakeBus specs and go vet ./core/services/testutil pass. The e2e dry run selects the intended 118 specs in one suite. The full CLI suite passes, as do 31 focused runs without coverage and 101 with coverage. git diff --check passes.

Still unresolved: the original Linux timeout in the mcp ci run wiring test did not reproduce in those runs. The VibeVoice job failed while downloading its Hugging Face model. Docker is unavailable here, so I could not execute the database/container suites. This is progress on the request, not a claim that every test is now green; leaving it in progress for the next invocation to check the new results.

Update 2026-09-08: resumed this request and pushed 4e4597dfc after merging master 561d62397. The remaining macOS worker test failed because the staging allow-list resolved symlinks in the requested file but not in the allowed directory. Both paths now resolve before comparison; the distributed-mode guide documents this behavior.

Reproduced the exact failure on Linux with a symlinked TMPDIR before the fix. Afterward, all 13 file-stager round-trip specs and four new containment cases pass with that TMPDIR, including rejection of sibling paths and symlinks escaping the allowed root. The focused worker file-staging tests, incoming virtual-model companion tests, go vet ./core/services/worker, and diff checks pass. Full lint could not run: this environment has neither make nor golangci-lint.

The existing Linux, distributed, cluster, and VibeVoice test runs were cancelled, so full verification remains incomplete. No CI jobs were restarted or polled, and this request remains in progress.

@localai-org-maint-bot

Copy link
Copy Markdown
Collaborator Author

This is a large PR (100 files) with tests-linux and tests-apple CANCELLED (required checks), plus a gosec failure and backend build failures (magpie-tts-cpp, face-detect). Could you check if the cancelled tests are due to a cascading failure or a real issue? The gosec failure also needs investigation.

Comment thread tests/e2e/distributed/cluster/cluster.go Fixed
Comment thread tests/e2e/mock-backend/main.go Fixed
Comment thread tests/e2e/mock-backend/main.go Fixed
Comment thread tests/e2e/mock-backend/main.go Fixed
Comment thread core/http/endpoints/openai/inpainting.go Fixed
@mudler
mudler force-pushed the test/distributed-e2e-ci branch from 2b10294 to 59fab0b Compare September 19, 2026 18:32
@localai-org-maint-bot
localai-org-maint-bot force-pushed the test/distributed-e2e-ci branch 2 times, most recently from 6620f49 to d992f8c Compare September 26, 2026 11:05
Starting a Postgres and a NATS container per spec cost roughly 48 minutes of
startup across the 213 specs behind SetupInfra, which is why this suite was
never wired into CI. Containers move to BeforeSuite and isolation comes from
CREATE DATABASE, which the dbName argument already described.

Assisted-by: Claude Opus 5 [claude-code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
A failed CREATE DATABASE panics out of the assertion before closeDB runs,
leaking a pgx pool per attempt. With --flake-attempts 5 that exhausts
postgres:16-alpine's 100 connection slots, at which point the cleanup path's
own Expect fails the spec and one hiccup cascades across the suite. Scope the
admin handle so the panic unwinds through defer closeDB, and let cleanup use a
fallible tryAdminDB that reports rather than asserts.

Register DeferCleanup immediately after CREATE so a later failure cannot leave
the database behind, and warn on TestInfra that the container handles are now
suite-wide.

Assisted-by: Claude Opus 5 [claude-code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The WebSocket log handler writes its "initial" batch before it calls
Subscribe, so a line appended the instant that batch arrives lands in the
circular buffer with no subscriber to receive it. Three backend-logs specs
append exactly there and then wait out a 5s read deadline; once a gorilla
read hits its deadline the connection is unusable, so the spec cannot retry.
`--focus='Worker WebSocket log streaming' --repeat=25` failed on attempt 17
with nothing else running, which is far too often to wire into CI.

Add BackendLogStore.SubscriberCount, resolving a model ID by the same
exact-key and replica-prefix rules Subscribe uses, and have the specs poll it
until the handler has attached. Nothing in production calls it and no
assertion is weakened; the handler's own snapshot/subscribe window is left as
it is, being a production streaming question rather than a test one.

Verified with 60 repeats of the WebSocket specs and three consecutive
--randomize-all runs of the whole distributed suite, all at
--flake-attempts 1: 239 of 240 specs pass in about 80 seconds.

Assisted-by: Claude Opus 5 [claude-code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
… works around

Three corrections from review of the previous commit.

The lock-order comment on SubscriberCount claimed no path takes s.mu and a
buffer lock together. Subscribe does exactly that, holding s.mu.RLock across
replica registrations that take buf.mu. State the rule that is actually true —
s.mu precedes any buffer lock, so counting after releasing it preserves the
order — and say what follows from it: the total is a sample, not a snapshot.

waitForLogSubscriber read as general-purpose but unblocks on the first
registered subscription. Subscribe attaches the exact-key buffer and each
replica buffer one at a time, so for a replicated model the count goes positive
while later replicas are still unattached and the race survives. Rename it
waitForSingleLogSubscriber, document that it holds only where Subscribe
resolves to one buffer, and assert on exactly 1: misuse then fails loudly on
the count rather than going quietly back to being flaky. Taking the expected
count as a parameter was the alternative, but that makes callers predict a
store-internal number and an under-count fails the same silent way as the
original bug.

The snapshot-then-subscribe race had no artifact outside a report, and review
found a second site carrying it. Mark both handlers identically, including the
point that swapping the two calls duplicates rather than drops and so is not
the fix. The race itself is left alone; this branch stays test infrastructure.

Assisted-by: Claude Opus 5 [claude-code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The suite has never run in CI, so 239 specs across 32 files were verified only
by hand. Path-filtered to distributed code, advisory until it earns a track
record, and with flake retries at 1 rather than 5 so nondeterminism surfaces
instead of being retried away.

Assisted-by: Claude Opus 5 [claude-code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The path allowlist covered 13 of the 99 packages the suite reaches. Commit
1dc3aee touched core/config, core/services/modeladmin and core/backend and
matched no entry, so it would have merged without running the very specs that
cover it. Use the paths-ignore denylist tests-e2e.yml already uses.

Disable the testcontainers reaper: the runner is ephemeral, so the reaper buys
nothing and its unpinned image was pulled mid-suite, defeating the pre-pull.

Drop continue-on-error, which no other workflow uses and which reports a failed
run as green. The job is advisory by staying out of branch protection instead.
Pin Go to 1.26.0 to match go.mod, and add the tmate-on-failure step.

Assisted-by: Claude Opus 5 [claude-code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Runs local-ai as real child processes, one per frontend replica and one per
worker, against containerised infrastructure. The in-process suites cannot
express frontend-replica failure: there is no process to kill and no real HTTP
boundary between a worker and the frontend it registered with.

Assisted-by: Claude Opus 5 [claude-code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Restarting a frontend replica must not move it: workers read
LOCALAI_REGISTER_TO once at boot and never re-resolve it, so a replica that
returns on a fresh port is unreachable by the workers that registered with it.
startFrontend now takes the port, with <= 0 meaning "allocate".

Process logs are opened for append rather than truncated, so a restarted
process cannot erase the log of the instance that died, which is the log a
failover post-mortem needs. The post-SIGKILL wait is bounded, so one stuck
child no longer becomes a suite-wide timeout that names nothing. Stop is
nil-safe because Start returns a nil cluster after stopping itself.

Start's doc comment no longer claims to wait for worker registration; that
needs an authenticated admin session, so it now says callers must poll
/api/nodes themselves.

Assisted-by: Claude Opus 5 [claude-code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The register handler answers 201 both for "user created, here is your
session" and for "this email already exists", so the status code cannot
tell a fresh registration from a repeat one. Key on the session cookie
instead and fall through to login when it is absent.

Assisted-by: Claude Opus 5 [claude-code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
… secret

Session rows are keyed by an HMAC of the token under a secret generated
per instance into {DataPath}/.hmac_secret. The replicas shared that
secret only because they shared a working directory, and that directory
was the source tree. Give each frontend LOCALAI_DATA_PATH under its own
baseDir and pin LOCALAI_AUTH_HMAC_SECRET, so a session minted at one
replica resolves at every other one by construction.

Assisted-by: Claude Opus 5 [claude-code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
…ness

The point of running LocalAI as real child processes is to be able to take
one away. Add KillFrontend (SIGKILL, the lost replica), StopFrontendGracefully
(SIGTERM, the rolling update), KillWorker, RestartFrontend and FrontendAlive.

RestartFrontend pins the dead replica's original port. Workers read
LOCALAI_REGISTER_TO once at boot and never re-resolve it, so a replica that
returns on a fresh port is unreachable by exactly the workers that registered
with it and the failover under test never happens.

It also wipes the replica's data directory, so the process comes back with
empty local state and has to rehydrate node, session and job state from the
shared Postgres and NATS. Reusing the directory would model a pod with a
persistent volume and hide the class of bug these tests exist to find. That
is only safe because the harness pins LOCALAI_AUTH_HMAC_SECRET; otherwise the
wipe would take {DataPath}/.hmac_secret with it and every session minted
before the restart would 401 afterwards.

FrontendAlive consults the reaper's exited channel before signal 0: a child
that has died but has not yet been waited on is a zombie, and signal 0 to a
zombie succeeds, which would report a dead replica as alive.

The new specs cover argument validation only. Killing, stopping and
restarting a live process needs a built binary plus Postgres and NATS, so
those paths stay unexecuted until the failover suites land.

Assisted-by: Claude Opus 5 [claude-code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
…he wipe

Review round 1. Comments only, plus one guard.

The note on Process.alive claimed the exited check closed the zombie window.
It does not. The reaper closes exited only after Cmd.Wait returns, and Wait
marks the os.Process done before returning, so exited being closed implies
signal 0 already errors and the branch cannot fire earlier than the one it
precedes. The window between the child exiting and waitid collecting it stays
open in both versions, and the only real mitigation is for callers to poll
with Eventually rather than sample once. Keep the check as hygiene, say what
it actually does, and say it again on the exited field, so nobody reads the
old claim and drops the Eventually.

Record what the cold wipe destroys. The harness sets no LOCALAI_STORAGE_URL,
so the object store is a directory under DataPath, and quantization and
fine-tune outputs live there too. Postgres keeps the job row; the artifact it
points at does not survive the restart. A spec that asserts otherwise will
fail for a storage reason wearing a failover costume.

Tell callers to let a graceful stop finish before restarting: RestartFrontend
terminates with SIGKILL, so pairing it straight after StopFrontendGracefully
cuts the drain short and silently converts the rolling-update case into the
crash case.

Refuse to wipe when the cluster has no work dir. frontendDataDir is relative
when baseDir is empty, so a Cluster built by some future test helper without
one would have RemoveAll walking frontend-N/data inside the source tree. The
guard sits before terminate, so a refusal leaves the cluster as it was.

Assisted-by: Claude Opus 5 [claude-code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Tasks 4 to 6 built a harness that runs local-ai as real child processes, but
none of it had ever started a process: every spec so far returned inside
argument validation. These two specs are the first to run it against a real
binary, a real Postgres and a real NATS.

Two frontends against one database both see a worker that registered through
only one of them. Every failover spec assumes this, so it is asserted first.

One admin session is minted at frontend 0 and reused for both replicas rather
than registering per frontend. The auth routes share a five-per-minute-per-IP
limiter and all e2e traffic is 127.0.0.1, so a session per frontend would
exhaust the budget as soon as a spec needs a third one. Reuse is sound because
sessions live in the shared Postgres and the harness pins one HMAC secret
across replicas; frontend 1 answering /api/nodes with 200 on a cookie minted at
frontend 0 is what proves it.

The binaries are resolved before SetupInfra so a missing build skips without
first provisioning a database the skip would then have to tear down.

Assisted-by: Claude Opus 5 [claude-code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The Cluster label partition is these two specs and nothing else, so a missing
binary skipped the entire job. Ginkgo exits 0 on skips, so a build step that
broke or moved its output would have left the job reporting "0 Passed |
2 Skipped" and going green without ever starting a cluster: the silent pass
this suite exists to make impossible. Skipping stays the local default, which
is the right courtesy for someone who has not run `make build`, but
LOCALAI_E2E_REQUIRE_BINARIES turns it into a failure that names the missing
path and the target that builds it. A value that is set but unparseable counts
as on, since reading it as off would restore the very skip it disables.

Failures also name themselves now. The roster poll kept returning a bare nil on
error, so a 401 at the second replica, a decode failure and "the worker never
registered" all presented identically as an empty list. It now retains the last
error and the last roster and reports whichever happened, through a lazily
evaluated Gomega description that costs nothing until something fails.

Finally, the two-frontend spec no longer depends on the harness to mean what it
says. It asserts an unauthenticated GET /api/nodes at frontend 1 is refused,
which observes the admin gate instead of assuming it, and it compares the
worker's registration id across the two replicas rather than its name. A future
harness that registered every worker with every frontend would have kept a
name-only assertion green while it quietly stopped proving anything about
shared state.

Assisted-by: Claude Opus 5 [claude-code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
mudler and others added 18 commits September 27, 2026 03:05
Separate hosts cannot use frontend paths directly. Stage the remaining path-bearing requests and the original model config.

Async quantization cleanup can span replicas and requests. Persist each cleanup phase so failures retry without fetching completed output twice.

Reject root aliases and untrusted export entries before they can mutate frontend or worker files.

The compiled cluster suite now exercises these paths through both public routes and the authenticated peer relay.

Assisted-by: Codex:GPT-5 [apply_patch] [exec_command]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Bind cleanup to storage namespaces and generation-scoped quantization state. Validate backend outputs, prove inputs reach worker staging, and make failed-start cleanup retryable.

Assisted-by: Codex:GPT-5 [apply_patch] [exec_command]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Reconcile the binary conformance inventory with the live backend interfaces, generated gRPC descriptor, staging overrides, and topology table so protocol growth cannot pass without an explicit coverage decision.

Assisted-by: Codex:GPT-5 [apply_patch] [exec_command]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Discover direct FileStagingClient backend overrides from every non-test source file in the nodes package, and locate that package without relying on runtime source paths so coverage inventory checks survive trimpath builds.

Assisted-by: Codex:GPT-5 [apply_patch] [exec_command]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Exercise WebUI sessions, fail-closed registration, per-node tunnel credentials, manual approval, heartbeats, and scoped agent credentials through real frontend and worker processes.

Assisted-by: Codex:GPT-5 [apply_patch] [exec_command]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Keep pending agent workers alive with authenticated heartbeats while preserving approval as the tunnel and execution boundary. Exercise the real binary credential handoff through an authenticated inference and verify that credential remains non-admin.

Assisted-by: Codex:GPT-5 [apply_patch] [exec_command]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The cluster target identifies feature and machine-auth scenarios while
retaining one shared binary build. CI requires binaries, so neither
scenario can skip silently.

Record the current-master fixture baseline and measured local suite
budgets. Keep new conformance tests within the branch lint policy.

Assisted-by: Codex:GPT-5 [apply_patch] [exec_command]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Register the new quantization, mock fixture, and protocol inventory tests through package-consistent Ginkgo suites. Keep table case names in Ginkgo diagnostics and label the protocol inventory for distributed CI selection.

Refresh the current-master fixture baseline using the final test structure.

Assisted-by: Codex:GPT-5 [apply_patch] [exec_command]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Exercise normal cluster shutdown through the real binary signal handlers and retain the loaded mock backend PID so the conformance scenario proves teardown leaves no orphan. Keep SIGKILL as a bounded fallback and leave the explicit crash primitives unchanged.

Assisted-by: Codex:GPT-5 [apply_patch] [exec_command]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Record the authoritative Cluster label count and describe the restart helper's bounded graceful shutdown path accurately.

Assisted-by: Codex:GPT-5 [apply_patch] [exec_command]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Upgrade the GGUF parser to reject string and array lengths larger than the remaining remote file before allocating them. This prevents malformed or inconsistent range responses from terminating LocalAI during background gallery warming.

Assisted-by: Codex:GPT-5 [apply_patch] [golangci-lint]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Keep go.sum aligned with the v0.26.3 parser version selected by go.mod.

Assisted-by: Codex:GPT-5 [apply_patch] [exec_command]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Tighten the permissions that can be tightened and justify the ones that
cannot. The e2e harness creates its directories 0750 and writes its model
config and process logs 0600; the copied binary keeps 0755 because the
suite executes it.

Everything else is a gosec rule firing on code that is doing what it is
supposed to: jitter drawn from math/rand for a reconnect backoff, a fixed
credential for a throwaway cluster, and subprocesses launched from the
binary the suite just built. Each carries a #nosec naming the rule and
the reason, in the form the tree already uses.

Scanned before and after: 13 findings in the packages this branch added,
now 0. The one that remains in core/services/worker is byte-identical on
master and is not this branch's to silence.

Assisted-by: Claude Opus 5 [claude-code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The frame length conversion is already bounded by the guard three lines
above it, which caps a payload at 1024 bytes and so cannot truncate a
uint16. The agent pick spreads work across workers that are equivalent by
construction, and predicting it buys an attacker nothing.

Both carry a #nosec naming the rule and the reason rather than a silent
suppression, matching the rest of the tree.

Assisted-by: Claude Opus 5 [claude-code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Master added Animate3D, negative_prompt, and context_size after this
branch diverged. The old suite did not exercise those paths, and Kokoros
no longer implemented the generated service trait.

Extend binary conformance across the tunnel owner and peer relay. Allow
long development versions so rebased binaries can register in PostgreSQL.
Clear the security findings introduced by the branch's new code.

Assisted-by: Codex:GPT-5 [apply_patch] [exec_command]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Use the existing canonical temp directory helper for the combined HTTP
and S3 staging fixture. macOS exposes its temp root through /var, which
the ephemeral-root validation rejects as a symlink.

Assisted-by: Codex:gpt-6
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Open the worker drawer and local-machine scale-out card before checking
the commands they contain.

Assisted-by: Codex:gpt-6
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
An empty roster opens the worker drawer automatically. Clicking Add
worker again targets the button behind the drawer and times out.
Wait for the drawer before checking the backend and agent commands.

The original test reproduces the CI failure. All six nodes-roster
Playwright tests pass with the visibility assertion.

Assisted-by: Codex:gpt-6
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Keep the newer SQLite dependency from master to resolve the conflict.

Assisted-by: Codex:gpt-6
Keep the version-reporting import from master and omit the unused
sanitize import after the transport changes.

Assisted-by: Codex:GPT-6
Preserve failover support alongside the PostgreSQL broadcast carrier.

Assisted-by: Codex:gpt-6 golangci-lint
Bring the distributed branch onto current master before the CI fix.

Assisted-by: Codex:gpt-6
A validated model path does not validate its adjacent hash metadata.
Reject symlinked metadata on reads and replace sidecars atomically
through a directory handle, without writing to symlink targets.

Regression tests reproduce forged hashes from external sidecars.
Focused file-transfer tests pass. The targeted gosec scan reports
20 fewer findings; remaining findings are not suppressed.

Assisted-by: Codex:gpt-6
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Read event snapshots through the fake transport accessor. The merged
scene and diarization tests still used removed fields and methods.

Assisted-by: Codex:GPT-6 go-vet
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Convert the hash sidecar tests to the required Ginkgo and Gomega style.
This resolves the forbidigo CI failures and preserves all four cases.

Assisted-by: Codex:gpt-6 golangci-lint
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Preserve multipart language hints alongside JSON diarization and speaker
profiles when resolving the endpoint conflict.

Assisted-by: Codex:gpt-6

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants