mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
feat(addons): list installed add-ons and preview releases for host access
The host access page needs the verified requests of each installed add-on, the operation that completed its generation, optional operations an upgrade added, and an upgrade that waits for approval. It also needs to read a release's manifest without installing it. Add both to the manager. An automatic upgrade that is held for approval is remembered. Assisted-by: Claude:claude-sonnet-5-5 go
This commit is contained in:
3 files changed
+165
No files matched your search
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
@@ -80,6 +81,9 @@ func (m *Manager) AutoUpgrade(ctx context.Context) []AutoUpgradeResult {
|
||||
if r.Err != nil {
|
||||
var refusal *HostRequirementsRefusal
|
||||
r.HeldForApproval = errors.As(r.Err, &refusal) && !refusal.Blocked()
|
||||
if r.HeldForApproval {
|
||||
m.holdUpgrade(id, st.CandidateVersion, append(slices.Clone(refusal.Summary.MissingRequired), refusal.Summary.MissingOptional...))
|
||||
}
|
||||
m.rememberAutoFailure(id, st.CandidateDigest)
|
||||
xlog.Error("Failed to auto-upgrade add-on", "addon", id, "from", st.InstalledVersion, "to", st.CandidateVersion, "error", r.Err)
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
package addons
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"github.com/mudler/LocalAI/core/services/addons/catalog"
|
||||
"github.com/mudler/LocalAI/core/services/addons/packages"
|
||||
addonruntime "github.com/mudler/LocalAI/core/services/addons/runtime"
|
||||
)
|
||||
|
||||
// OfficialReceipt reports whether a package came from the official add-on
|
||||
// library that LocalAI ships and carries the official signature. The source ID
|
||||
// alone is not enough, since an administrator names libraries freely.
|
||||
func OfficialReceipt(r packages.PackageReceipt) bool {
|
||||
return r.SourceID == catalog.OfficialLibraryID && r.Verification == "sha256+sigstore" && catalog.IsOfficialAddonURL(r.SourceURL)
|
||||
}
|
||||
|
||||
// HostAccessInstalled is an installed add-on as the host access page needs it.
|
||||
type HostAccessInstalled struct {
|
||||
AddonID, Title, Version, State, Identity, SourceID string
|
||||
Generation uint64
|
||||
// OperationID is the lifecycle operation that completed the installed
|
||||
// generation.
|
||||
OperationID string
|
||||
Official bool
|
||||
Required, Optional []string
|
||||
// NewOptional are optional operations that the installed version asks for,
|
||||
// that the generation before did not, and that nothing grants yet.
|
||||
NewOptional []string
|
||||
// HeldVersion and HeldDelta describe an upgrade that waits for approval.
|
||||
HeldVersion string
|
||||
HeldDelta []string
|
||||
}
|
||||
|
||||
type heldUpgrade struct {
|
||||
version string
|
||||
delta []string
|
||||
}
|
||||
|
||||
func (m *Manager) holdUpgrade(id, version string, delta []string) {
|
||||
m.autoMu.Lock()
|
||||
defer m.autoMu.Unlock()
|
||||
if m.heldUpgrades == nil {
|
||||
m.heldUpgrades = map[string]heldUpgrade{}
|
||||
}
|
||||
m.heldUpgrades[id] = heldUpgrade{version: version, delta: slices.Clone(delta)}
|
||||
}
|
||||
|
||||
func declared(r packages.PackageReceipt) (required, optional []string) {
|
||||
if h := r.Manifest.Capabilities.HostOperations; h != nil {
|
||||
return slices.Clone(h.Required), slices.Clone(h.Optional)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// HostAccessInstalled lists the installed add-ons with the host operations
|
||||
// their verified manifests ask for. It never reads a grant: the caller asks
|
||||
// the Authority.
|
||||
func (m *Manager) HostAccessInstalled(ctx context.Context) ([]HostAccessInstalled, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if e := m.check(ctx); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if m.uncertain || m.runtimeRecoveryRequired {
|
||||
return nil, ErrRecoveryRequired
|
||||
}
|
||||
var out []HostAccessInstalled
|
||||
for id, st := range m.journal.Addons {
|
||||
r, ok := currentReceipt(st)
|
||||
if !ok || !st.Installed {
|
||||
continue
|
||||
}
|
||||
in := HostAccessInstalled{AddonID: id, Title: id, Version: r.Manifest.Version, Identity: st.Identity, SourceID: r.SourceID,
|
||||
Generation: st.Generation, Official: OfficialReceipt(r), State: addonruntime.StateStopped}
|
||||
if u := r.Manifest.Capabilities.UI; u != nil && u.Title != nil && u.Title.Default() != "" {
|
||||
in.Title = u.Title.Default()
|
||||
}
|
||||
if st.DesiredEnabled && m.runtime != nil {
|
||||
in.State = m.runtime.State(id, st.Generation)
|
||||
}
|
||||
if m.admit(r) != nil {
|
||||
in.State = "failed"
|
||||
}
|
||||
in.Required, in.Optional = declared(r)
|
||||
for _, g := range st.Generations {
|
||||
if g.Number+1 == st.Generation {
|
||||
pr, po := declared(g.Receipt)
|
||||
for _, op := range in.Optional {
|
||||
if !slices.Contains(pr, op) && !slices.Contains(po, op) {
|
||||
if ok, _ := m.hostAuthority().Allows(id, op); !ok {
|
||||
in.NewOptional = append(in.NewOptional, op)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for oid, op := range m.journal.Operations {
|
||||
if op.Command.AddonID == id && op.Status.Phase == string(lifecycleCompleted) && op.Status.Generation == st.Generation &&
|
||||
(op.Command.Action == "install" || op.Command.Action == "upgrade") && (in.OperationID == "" || oid < in.OperationID) {
|
||||
in.OperationID = oid
|
||||
}
|
||||
}
|
||||
m.autoMu.Lock()
|
||||
if h, ok := m.heldUpgrades[id]; ok && h.version != in.Version {
|
||||
in.HeldVersion, in.HeldDelta = h.version, slices.Clone(h.delta)
|
||||
}
|
||||
m.autoMu.Unlock()
|
||||
out = append(out, in)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].AddonID < out[j].AddonID })
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// HostAccessRelease is what the verified, staged package of a release asks for.
|
||||
type HostAccessRelease struct {
|
||||
AddonID, Title, Version, Digest string
|
||||
Official, Installed bool
|
||||
Required, Optional []string
|
||||
}
|
||||
|
||||
// PreviewRelease resolves and stages a release, reads its verified manifest
|
||||
// and discards the stage. It changes no state of the add-ons.
|
||||
func (m *Manager) PreviewRelease(ctx context.Context, sourceID, addonID, version string) (HostAccessRelease, error) {
|
||||
if m.installer == nil {
|
||||
return HostAccessRelease{}, ErrForbidden
|
||||
}
|
||||
m.opMu.Lock()
|
||||
defer m.opMu.Unlock()
|
||||
m.mu.Lock()
|
||||
if e := m.check(ctx); e != nil {
|
||||
m.mu.Unlock()
|
||||
return HostAccessRelease{}, e
|
||||
}
|
||||
installed := m.journal.Addons[addonID].Installed
|
||||
m.mu.Unlock()
|
||||
r, e := m.resolveRelease(ctx, LifecycleCommand{SourceID: sourceID, AddonID: addonID, Version: version})
|
||||
if e != nil {
|
||||
return HostAccessRelease{}, e
|
||||
}
|
||||
staged, e := m.installer.Stage(ctx, r, nil)
|
||||
if e != nil {
|
||||
return HostAccessRelease{}, e
|
||||
}
|
||||
rc := staged.Receipt()
|
||||
if e = m.installer.Discard(staged); e != nil {
|
||||
return HostAccessRelease{}, e
|
||||
}
|
||||
out := HostAccessRelease{AddonID: rc.Manifest.ID, Title: rc.Manifest.ID, Version: rc.Manifest.Version, Digest: rc.Digest,
|
||||
Official: OfficialReceipt(rc), Installed: installed}
|
||||
if u := rc.Manifest.Capabilities.UI; u != nil && u.Title != nil && u.Title.Default() != "" {
|
||||
out.Title = u.Title.Default()
|
||||
}
|
||||
out.Required, out.Optional = declared(rc)
|
||||
return out, nil
|
||||
}
|
||||
@@ -153,6 +153,8 @@ type Manager struct {
|
||||
// automatic upgrade that failed. In memory only: a restart tries again.
|
||||
autoMu sync.Mutex
|
||||
autoFailures map[string]string
|
||||
// heldUpgrades holds, per add-on, an upgrade that waits for approval.
|
||||
heldUpgrades map[string]heldUpgrade
|
||||
// skillSync keeps the chats' skill layer in step with the installed
|
||||
// packages. Nil until the chats' files exist (see SetSkillSync).
|
||||
skillSync SkillSync
|
||||
|
||||
Reference in new issue
Block a user