Skip to content

feat(backends): --disable-backend-install disables backend downloads. - #12754

Open
blackd wants to merge 1 commit into
mudler:masterfrom
blackd:disable-backend-install
Open

blackd wants to merge 1 commit into
mudler:masterfrom
blackd:disable-backend-install

Conversation

@blackd

@blackd blackd commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Some deployments must not acquire executable code at runtime: production systems that run exactly the backend set they were tested with, air-gapped or locked-down hosts, immutable images, and hosts whose backends come from a package manager via --backends-system-path. Today LocalAI will install a backend on its own in many situations — a gallery click, a model install, a model load on a worker, the background auto-upgrader — and nothing lets an operator forbid that.

This adds --disable-backend-install / LOCALAI_DISABLE_BACKEND_INSTALL (hardening group, off by default, not a runtime setting). With it on:

  • Enforcement at the bottom: gallery.InstallBackend, InstallBackendFromGallery and UpgradeBackend return a typed ErrBackendInstallDisabled, so every path ends in a refusal — gallery/OCI/URL/path installs, upgrades (manual, update-all, auto-upgrader), the LOCALAI_EXTERNAL_BACKENDS boot loop, the Assistant's install_backend/upgrade_backend, local-ai backends upgrade. Backends already installed, system-path ones included, keep working: the "already present and runnable" short-circuit runs before the guard.
  • Models whose backend is missing are refused, not half-installed: gallery installs get a 409 {"code":"backend_not_installed","backend":…} up front; every other path (import, preload, the Assistant's tools) is refused inside InstallModel right after the config is validated — before any artifact or file is downloaded — with an error naming the backend. A model that names no backend passes, as the runtime picks one at load.
  • HTTP admission: one middleware answers 403 {"code":"backend_install_disabled"} on the seven install/upgrade routes (/backends/apply, /backends/upgrade/:name, /api/backends/install/:id, /api/backends/install-external, /api/backends/upgrade/:name, /api/nodes/:id/backends/{install,upgrade}), so clients get an immediate answer instead of a failed job.
  • Distributed mode: the worker gets the same variable in its config and SystemState, so install-on-load and frontend-requested installs/upgrades are refused where the bytes would land; the frontend's DistributedBackendManager refuses fan-outs up front. The standalone local-ai worker … subcommands honour it too (they already prefer a present backend; a missing one now exits with the error instead of installing).
  • No pointless polling: the upgrade checker isn't started when installs are disabled.
  • Discovery and UI: backend_install in GET /api/features and the /.well-known/localai.json capabilities; the Backends page shows a notice, keeps browsing and delete, and offers no install/upgrade/manual-install/check actions; the Import Model page states that the import will be refused instead of promising a download.

Notes for Reviewers

  • The guard sits in three core/gallery functions plus one pre-download check in InstallModel, rather than at each caller; the HTTP middleware exists only to give clients an immediate answer.

  • When an install is refused, the op handler skips its "remove the half-installed directory" cleanup: nothing was written. That cleanup dates from feat(backends): add system backend, refactor #6059 (089efe0), when installs wrote directly into the backends path; since fix(gallery): make backend (re)install a clean replace instead of an overlay #10726 (1d5139f) installs stage into <name>.install-tmp and swap atomically, so the cleanup now removes a working backend after any failed forced reinstall, not only a refused one — a separate fix.

  • Swagger annotated (@Failure 403/409) and regenerated; the 403/409 bodies use the flat {error, code} shape concreteFanOutGuard already returns on /backends/apply.

  • MCP: the in-process client refuses up front so the Assistant gets the error on the tool call; the upgrade_backend skill and tool list say what to do on it.

  • Tests: gallery (typed error, nothing written, present backend accepted, upgrade refused, RequiredBackendInstalled, model refused before download, model accepted with its backend present), middleware (pass/403), UI routes through real registration (403 ×3, 409 naming the backend, pass-through for a backend-less entry), worker service (install and upgrade requests refused), worker CLI helper (present backend used, missing refused). Suites needing the PostgreSQL testcontainer weren't runnable in my environment (no Docker); their failure counts are unchanged by this change.

  • Docs: features/backends.md (new section), features/distributed-mode.md (worker note), reference/cli-reference.md (hardening row), features/api-discovery.md (capability).

  • Out of scope, planned as a follow-up: marking gallery entries whose backend is missing on the Models page and disabling their Install button (the 409 covers the server side today). The local-ai backends install / local-ai models install CLI subcommands remain deliberate operator actions.

  • Yes, I signed my commits.

  • Documentation updated (docs/content/) for user-facing changes, or not applicable

@blackd blackd changed the title feat(backends): --disable-backend-install refuses every backend acqui… feat(backends): --disable-backend-install disables backend downloads. Oct 17, 2026

@localai-org-maint-bot localai-org-maint-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Model admission checks the frontend filesystem even when inference runs on workers.

In core/gallery/backends.go:317-322 and core/gallery/models.go:676-683, backend availability is determined solely by ListSystemBackends(systemState). With the documented flag enabled on every frontend and worker, a normal distributed frontend with no local backend rejects a model whose vllm (or other) backend is already installed and runnable on a worker. Gallery admission returns 409; import/preload reaches the same local-only refusal in InstallModel. No backend download would have been needed.

NewDistributedModelManager explicitly disables local automatic backend installation because inference belongs to workers, but delegates model installation to LocalModelManager, so the new unconditional check still runs. Please make both admission and the lower-level model check aware of the execution target, while keeping actual backend installation disabled. Add a regression with an empty frontend backend directory and a compatible worker with the required backend already installed, plus a case with no eligible installed backend. This needs coordinated model-admission/worker handling rather than a minor formatting fix.

Reviewed against master 48f358dd9. After regenerating protobuf bindings locally, go test ./core/gallery -run Test -ginkgo.focus="Disabled backend installation" -count=1 -timeout=45s, go vet ./core/gallery, and diff whitespace checks pass. The distributed finding is from tracing the real call path; I did not run a live cluster. @mudler this blocks my sign-off on the advertised distributed configuration.

@blackd
blackd force-pushed the disable-backend-install branch from b3df98b to 6e6eda2 Compare October 19, 2026 21:29
@blackd

blackd commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, indeed the model admission check read the frontend's own backend directory, which is empty in distributed mode, so every model install was refused there.

What changed

  • Availability is now a question for the active BackendManager (BackendAvailable(name)), not for the local filesystem. The local manager answers from the backend directory as before; the distributed manager answers from the workers' backend.list replies.
  • The HTTP admission check, InstallModel's pre-download refusal, the operation loop (stamped on each op), startup preloads (MODELS, PRELOAD_MODELS, --preload-models), the local-ai models install CLI and /api/models/config/:id all go through it.
  • A worker's NodeBackendInfo now carries the backend's alias on the concrete's own row, so a model whose config names an alias is admitted when a worker holds the concrete. No extra row is added, so the upgrade checker still sees one row per installed backend (the phantom-alias case that backend.list strips stays stripped).
  • Only healthy workers count, matching what the router will place a load on.
  • Docs updated for both pages.

Tests

  • core/gallery: AvailableOnCluster (presence, alias, unhealthy node), install trusting a cluster availability, refusal before download.
  • core/http/routes: a distributed frontend with an empty backends directory accepts a model a worker can run (200) and refuses one no worker can (409).
  • core/startup: the same two cases through the preload path.
  • core/services/workerctl: wire round-trip pins the new alias field.

Small "problem", left as is

  • DistributedBackendManager.BackendAvailable fans backend.list out to every worker once per model install, on top of the fan-out the install itself does. That is one extra round of NATS requests per install, not per request, so I left it rather than add a cache with its own invalidation. Happy to change if you would rather it reuse a cached listing.

@blackd
blackd force-pushed the disable-backend-install branch from 6e6eda2 to 992ce88 Compare October 19, 2026 21:46
…sition

A hardening switch for deployments that must run exactly the backend set they
were tested with (production, air-gapped, immutable images, package-managed
hosts). With LOCALAI_DISABLE_BACKEND_INSTALL=1 every install and upgrade path
refuses — gallery, OCI/URL/path, node-scoped, auto-upgrade, boot-time external
backends — HTTP routes answer 403 up front, and a model whose backend is
missing is refused before any download: 409 on the API, an error on preload
and import. Whether a backend is present is asked of the execution target:
the local backend directory on a single node, the workers' listings on a
distributed frontend, which installs nothing itself. Backends already
present, including system-path ones, keep working.

Assisted-by: Claude Code:claude-fable-5
Signed-off-by: Plamen K. Kosseff <p.kosseff@gmail.com>
@blackd
blackd force-pushed the disable-backend-install branch from 992ce88 to 6e1768c Compare October 20, 2026 04:02

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.

2 participants