fix(addons): refuse a reinstall that would migrate retained data unbacked

An upgrade takes a verified backup before the new package migrates
the data. A reinstall over retained data has no running generation
to stop, so the new package would migrate it without one. Refuse a
reinstall whose data format differs from the retained one.

Assisted-by: Claude Code:claude-sonnet-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto committed 2026-10-20 20:54:44 +00:00
1 parent 2de5ce7cae
commit faa74b7a92
2 files changed
+15

No files matched your search

+8
View File
@@ -364,6 +364,14 @@ func (m *Manager) Submit(ctx context.Context, actor contract.Actor, cmd Lifecycl
if last, ok := lastReceipt(m.journal.Addons[cmd.AddonID]); ok && recovery.CheckCompatible(last, staged.Receipt()) != nil {
return fail()
}
if cmd.Action != "upgrade" {
// A reinstall has no running generation to back up, and
// the candidate would migrate the retained data when it
// starts. Only an upgrade takes a verified backup first.
if last, ok := lastReceipt(m.journal.Addons[cmd.AddonID]); ok && recovery.FormatChanged(last, staged.Receipt()) {
return fail()
}
}
if cmd.Action == "upgrade" {
return m.upgradeWithRecovery(ctx, cmd, op, &next, staged, fail)
}
@@ -35,6 +35,13 @@ func formatChanged(prev, next packages.PackageReceipt) bool {
return next.Manifest.DataFormat.Max != prev.Manifest.DataFormat.Max
}
// FormatChanged reports whether next would migrate the data that prev wrote.
// A caller that cannot take a backup first (a reinstall has no running
// generation to stop) must refuse such a package rather than migrate unbacked.
func FormatChanged(prev, next packages.PackageReceipt) bool {
return prev.Manifest.ID != "" && formatChanged(prev, next)
}
// migrationState reads the status file the add-on writes while it migrates.
// The bool is false when it wrote none.
func (r *Recovery) migrationState(addonID string) (string, string, bool) {