fix(plugin): make in-place plugin upgrades rebind commands (W2-3)

installFromDisk replaced r.plugins/r.byName with a fresh *Instance but
left r.commands keyed to the old pointer and the old module running:
re-installing an enabled plugin blocked its own command re-registration
(RegisterCommand compared ownership by pointer) and kept dispatch routing
into the orphaned module until restart. Reinstall now deactivates the old
instance and clears its bindings, and RegisterCommand compares ownership
by plugin identity (manifest name) — the same plugin re-binds freely, a
different plugin still cannot hijack an owned command.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
J3vb
2026-07-19 08:52:31 +02:00
co-authored by Claude Fable 5
parent 47663e2be3
commit 2a4b2e1628
2 changed files with 21 additions and 1 deletions
+6 -1
View File
@@ -33,7 +33,12 @@ func (r *Registry) RegisterCommand(cmd string, inst *Instance) error {
}
r.mu.Lock()
defer r.mu.Unlock()
if existing, ok := r.commands[cmd]; ok && existing != inst {
// Ownership is compared by plugin identity (manifest name — unique per
// registry), not instance pointer: an in-place upgrade replaces the
// *Instance, and the same plugin must be able to re-bind its own
// commands. A *different* plugin claiming an owned command is still
// refused (cross-plugin command-hijack protection).
if existing, ok := r.commands[cmd]; ok && existing.Manifest.Name != inst.Manifest.Name {
return fmt.Errorf("plugin: command %q already registered by %q", cmd, existing.Manifest.Name)
}
r.commands[cmd] = inst
+15
View File
@@ -183,6 +183,21 @@ func (r *Registry) installFromDisk(ctx context.Context, found foundPlugin) error
}
r.mu.Lock()
defer r.mu.Unlock()
// Re-install: tear down the old instance and drop its command bindings.
// Bindings are keyed to the old *Instance, so leaving them in place both
// blocked the fresh instance from re-registering its own commands and
// kept dispatch routing into the orphaned old module until restart.
if old := r.byName[found.Manifest.Name]; old != nil {
r.platformDeactivate(old)
for cmd, owner := range r.commands {
if owner == old {
delete(r.commands, cmd)
}
}
if old.ID != id {
delete(r.plugins, old.ID)
}
}
inst := &Instance{
ID: id,
Manifest: found.Manifest,