fix: register core capabilities + plugin lifecycle before discovery

Core fix: core capabilities (plugin-manager, capability-registry,
contribution-registry, permissions, events) now registered BEFORE plugin
discovery, so plugins can resolve required capabilities at load time.

Changes:
- main.go: register 5 core capabilities before DiscoverPlugins();
  add plugin lifecycle (register caps, resolve required/optional,
  set status LOADED/DEGRADED/MISSING_REQUIRED, register contributions)
- api/app.go: ReloadPlugins now re-registers core + plugin capabilities
- capability/registry.go: add UnregisterAll() for reload
- cmd/smoke-platform/main.go: verify core caps, required resolution,
  degraded status, total caps >= 7
- PluginCard.svelte: remove ✗ on optional-missing, show degraded info
- PluginManager.svelte: preserved (counter shows core+plugin caps)

Rule: optional missing => DEGRADED (not FAILED).
Required missing => MISSING_REQUIRED_CAPABILITY.

Verified: smoke-platform , test.sh (10/10) , check.sh 
This commit is contained in:
2026-06-16 17:12:46 +08:00
parent 1c75389535
commit dd3a5f8ff5
4 changed files with 215 additions and 5 deletions
+49
View File
@@ -2,6 +2,7 @@
package api
import (
"fmt"
"log"
"os"
"path/filepath"
@@ -108,7 +109,55 @@ func (a *App) ReloadPlugins() (int, string) {
log.Printf("[api] ReloadPlugins: scanning dirs: %v", discoveryDirs)
// Unregister all non-core capabilities
a.capRegistry.UnregisterAll()
// Re-register core capabilities
coreCaps := []string{
"verstak/core/plugin-manager/v1",
"verstak/core/capability-registry/v1",
"verstak/core/contribution-registry/v1",
"verstak/core/permissions/v1",
"verstak/core/events/v1",
}
if err := a.capRegistry.Register("verstak-desktop", coreCaps); err != nil {
log.Printf("[api] ReloadPlugins: failed to re-register core capabilities: %v", err)
}
plugins, errs := plugin.DiscoverPlugins(discoveryDirs)
// Plugin lifecycle: register capabilities + contributions
for i := range plugins {
p := &plugins[i]
if len(p.Manifest.Provides) > 0 {
if err := a.capRegistry.Register(p.Manifest.ID, p.Manifest.Provides); err != nil {
log.Printf("[plugin] %s: capability registration failed: %v", p.Manifest.ID, err)
p.Status = plugin.StatusFailed
p.Error = err.Error()
continue
}
}
missingRequired := a.capRegistry.CheckRequired(p.Manifest.Requires)
if len(missingRequired) > 0 {
p.Status = plugin.StatusMissingRequiredCapability
p.Error = fmt.Sprintf("missing required: %s", strings.Join(missingRequired, ", "))
continue
}
missingOptional := a.capRegistry.CheckRequired(p.Manifest.OptionalRequires)
if len(missingOptional) > 0 {
p.Status = plugin.StatusDegraded
} else {
p.Status = plugin.StatusLoaded
}
if p.Manifest.Contributes != nil {
a.contribRegistry.Register(p.Manifest.ID, p.Manifest.Contributes)
}
}
a.plugins = plugins
var buf strings.Builder
+7
View File
@@ -46,6 +46,13 @@ func (r *Registry) Register(pluginID string, capabilities []string) error {
return nil
}
// UnregisterAll removes all capabilities (used before reload).
func (r *Registry) UnregisterAll() {
r.mu.Lock()
defer r.mu.Unlock()
r.capabilities = make(map[string]*Entry)
}
// Unregister removes all capabilities provided by a plugin.
func (r *Registry) Unregister(pluginID string) {
r.mu.Lock()